{"slug": "build-a-hybrid-keyword-vector-search-engine-with-typesense", "title": "Build a Hybrid Keyword + Vector Search Engine with Typesense", "summary": "Typesense 30.2 can now fuse BM25 keyword ranks with local embedding search in a single API call, eliminating the need for a separate embedding service. A tutorial by Rachel Goldstein demonstrates building a hybrid search engine that indexes a movie catalog with auto-generated embeddings and runs hybrid queries that surface results keyword-only search misses.", "body_md": "# Build a Hybrid Keyword + Vector Search Engine with Typesense\n\nFuse BM25 keyword ranks with local embedding search in one Typesense API call for better relevance.\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)\n\n## What you'll build\n\nYou'll stand up a local [Typesense](https://typesense.org) search engine, index a small movie catalog with **auto-generated embeddings**, and run a single query two ways: pure BM25 keyword search, then a **hybrid** search that fuses keyword ranks with semantic vector ranks. By the end you'll see the hybrid query surface results that keyword-only search misses — and you'll know how to tune the keyword/vector balance with one parameter.\n\nTypesense is unusual here: it generates query and document embeddings *inside the server* (built-in ONNX runtime, CPU only), so you never run a separate embedding service. Keyword search, vector search, and rank fusion all happen in one API call.\n\n## Prerequisites\n\nVerified against these versions on 2026-07-23:\n\n**Typesense Server 30.2**(Docker image`typesense/typesense:30.2`\n\n)**Docker** 24+ ([install](https://docs.docker.com/get-docker/)) — works on Apple Silicon and x86_64**curl** andfor sending requests and reading JSON[jq](https://jqlang.github.io/jq/)- ~2 GB free RAM and an internet connection on first index (Typesense downloads the embedding model once)\n\nNo GPU, no Python, no OpenAI key required — the embeddings are computed locally by Typesense.\n\n## 1. Start Typesense\n\n```\nexport TYPESENSE_API_KEY=xyz\nmkdir -p \"$(pwd)/typesense-data\"\ndocker run -d --name typesense -p 8108:8108 \\\n  -v \"$(pwd)/typesense-data:/data\" \\\n  typesense/typesense:30.2 \\\n  --data-dir /data --api-key=\"$TYPESENSE_API_KEY\" --enable-cors\n```\n\nConfirm it's healthy:\n\n```\ncurl -s http://localhost:8108/health\n```\n\nYou should get `{\"ok\":true}`\n\n.\n\n## 2. Create a collection with an auto-embedding field\n\nThe `embed`\n\nblock tells Typesense to build a vector for each document from the `title`\n\nand `overview`\n\nfields. `ts/all-MiniLM-L12-v2`\n\nis a built-in model (384 dimensions); the `ts/`\n\nprefix means Typesense fetches and runs it for you — no `num_dim`\n\nneeded when you use auto-embedding.\n\n```\ncurl -s \"http://localhost:8108/collections\" \\\n  -H \"X-TYPESENSE-API-KEY: $TYPESENSE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"movies\",\n    \"fields\": [\n      {\"name\": \"title\",    \"type\": \"string\"},\n      {\"name\": \"overview\", \"type\": \"string\"},\n      {\"name\": \"embedding\",\"type\": \"float[]\",\n        \"embed\": {\n          \"from\": [\"title\", \"overview\"],\n          \"model_config\": {\"model_name\": \"ts/all-MiniLM-L12-v2\"}\n        }\n      }\n    ]\n  }'\n```\n\n## 3. Index the documents\n\nTypesense imports [JSONL](https://jsonlines.org/) (one document per line). The **first** import triggers the model download (~120 MB), so this call can take 30–60 seconds; later imports are instant.\n\n```\ncurl -s \"http://localhost:8108/collections/movies/documents/import?action=create\" \\\n  -H \"X-TYPESENSE-API-KEY: $TYPESENSE_API_KEY\" \\\n  -H \"Content-Type: text/plain\" \\\n  --data-binary $'{\"id\":\"1\",\"title\":\"Interstellar\",\"overview\":\"A team of explorers travels through a wormhole in space in search of a new home for humanity.\"}\\n{\"id\":\"2\",\"title\":\"The Martian\",\"overview\":\"An astronaut is stranded on Mars and must survive alone until a rescue mission arrives.\"}\\n{\"id\":\"3\",\"title\":\"Gravity\",\"overview\":\"Two astronauts fight to survive after debris leaves them adrift in orbit above Earth.\"}\\n{\"id\":\"4\",\"title\":\"Jurassic Park\",\"overview\":\"Scientists clone dinosaurs to populate an island theme park that quickly goes wrong.\"}\\n{\"id\":\"5\",\"title\":\"Finding Nemo\",\"overview\":\"A timid clownfish searches the ocean to find his abducted son.\"}'\n```\n\nEach line comes back with `{\"success\":true}`\n\n.\n\n## 4. Run the keyword-only baseline\n\nSearch only the text fields — this is standard BM25.\n\n```\ncurl -s \"http://localhost:8108/multi_search\" \\\n  -H \"X-TYPESENSE-API-KEY: $TYPESENSE_API_KEY\" \\\n  -d '{\"searches\":[{\n    \"collection\":\"movies\",\n    \"q\":\"surviving alone in outer space\",\n    \"query_by\":\"title,overview\"\n  }]}' | jq '.results[0].hits[].document.title'\n```\n\nYou'll get only documents that literally contain query words — \"Interstellar\" (has \"space\"), \"The Martian\" (has \"survive alone\"), and \"Gravity\" (has \"survive\"). A film about being adrift with no shared keywords would be ranked purely on those word overlaps, and near-synonyms are invisible.\n\n## 5. Run the hybrid search\n\nAdd the `embedding`\n\nfield to `query_by`\n\n. Typesense now embeds your query string automatically, runs a nearest-neighbor search alongside the keyword search, and fuses the two result lists. The `vector_query`\n\nlets you set `alpha`\n\n— the weight given to the **vector** rank in the fusion (default `0.3`\n\n; keyword gets `1 − alpha`\n\n). Here we lean semantic at `0.7`\n\n.\n\n```\ncurl -s \"http://localhost:8108/multi_search\" \\\n  -H \"X-TYPESENSE-API-KEY: $TYPESENSE_API_KEY\" \\\n  -d '{\"searches\":[{\n    \"collection\":\"movies\",\n    \"q\":\"surviving alone in outer space\",\n    \"query_by\":\"title,overview,embedding\",\n    \"vector_query\":\"embedding:([], alpha: 0.7)\",\n    \"exclude_fields\":\"embedding\"\n  }]}' | jq '.results[0].hits[] | {title: .document.title, rrf: .hybrid_search_info.rank_fusion_score, dist: .vector_distance}'\n```\n\nThe empty `[]`\n\nin `embedding:([], alpha: 0.7)`\n\ntells Typesense to use the vector it auto-generated from `q`\n\nrather than a vector you supply. `exclude_fields:\"embedding\"`\n\nkeeps the 384-float array out of the response.\n\n## Verify it works\n\nThe hybrid response reorders and *expands* the result set. Each hit now carries a fused score plus a vector distance, similar to:\n\n```\n{\"title\": \"Gravity\",       \"rrf\": 0.85, \"dist\": 0.42}\n{\"title\": \"The Martian\",   \"rrf\": 0.72, \"dist\": 0.39}\n{\"title\": \"Interstellar\",  \"rrf\": 0.66, \"dist\": 0.31}\n{\"title\": \"Finding Nemo\",  \"rrf\": 0.30, \"dist\": 0.61}\n```\n\nTwo things confirm success:\n\n**Every hit has a**— even documents that shared no keywords with the query — proving the semantic leg ran. (Exact`vector_distance`\n\n`rank_fusion_score`\n\nand distance numbers will vary; the*presence*of both fields and the reordering is what matters.)**The ranking shifts** versus Step 4. Semantically strong matches like \"Gravity\" climb, while a keyword coincidence sinks. Re-run Step 5 with`alpha: 0.1`\n\nand the order snaps back toward the keyword baseline — that single knob is the whole point of rank fusion.\n\nTypesense builds the fused score from each document's *rank* in the two lists: `rank_fusion_score = (1 − alpha) · K + alpha · S`\n\n, where `K`\n\nand `S`\n\nare the keyword and semantic ranks. Set `rerank_hybrid_matches: true`\n\nin the search params to force both a `text_match`\n\nand a `vector_distance`\n\nonto every hit for finer control.\n\n## Troubleshooting\n\n** Property embed.model_config.model_name is not a valid string. on collection creation** — the\n\n`embed`\n\nblock is malformed or nested under the wrong field. It must sit inside the `float[]`\n\nfield definition, with `from`\n\nas an array of *existing string field names*declared in the same schema.\n\n**Import hangs or first request returns 503 Service Unavailable** — the model is still downloading.\n\n`ts/all-MiniLM-L12-v2`\n\nis fetched on the first index; watch progress with `docker logs -f typesense`\n\n. If it never completes, the container likely has no outbound network — the model download needs internet even though inference is local.** {\"code\":404,\"error\":\"Could not find a field named embedding in the schema.\"} on search** — you added\n\n`embedding`\n\nto `query_by`\n\non a collection created without the `embed`\n\nfield. Drop and recreate the collection (`curl -X DELETE .../collections/movies`\n\n) with the Step 2 schema.** vector_query returns Field embedding must have a vector query value.** — the\n\n`([], ...)`\n\nbrackets are missing or you passed a non-empty vector while relying on auto-embedding. For an auto-embedded query, keep the brackets empty: `embedding:([], alpha: 0.7)`\n\n.## Next steps\n\n**Filter + fusion:** add`filter_by`\n\n(e.g.`filter_by:\"year:>2010\"`\n\n) to constrain either leg before scoring.**Distance cutoffs:** use`distance_threshold`\n\ninside`vector_query`\n\nto drop weak semantic matches entirely.**Bring your own embeddings:** swap`model_config`\n\nfor an OpenAI/Cohere/GCP model, or index vectors you computed elsewhere and pass them via`vector_query`\n\n.- Read the full\n[Vector Search reference](https://typesense.org/docs/30.2/api/vector-search.html)and the[Semantic Search guide](https://typesense.org/docs/guide/semantic-search.html)for reranking, curation, and production sizing.\n\n## Sources & further reading\n\n-\n[Vector Search](https://typesense.org/docs/30.2/api/vector-search.html)— typesense.org -\n[Semantic Search](https://typesense.org/docs/guide/semantic-search.html)— typesense.org -\n[Install Typesense](https://typesense.org/docs/guide/install-typesense.html)— typesense.org -\n[typesense/typesense Docker Image](https://hub.docker.com/r/typesense/typesense/)— hub.docker.com\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)· Dev Tools Editor\n\nRachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/build-a-hybrid-keyword-vector-search-engine-with-typesense", "canonical_source": "https://sourcefeed.dev/a/build-a-hybrid-keyword-vector-search-engine-with-typesense", "published_at": "2026-07-23 11:45:14+00:00", "updated_at": "2026-07-23 11:56:46.185554+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "developer-tools"], "entities": ["Typesense", "Rachel Goldstein", "Docker", "ONNX"], "alternates": {"html": "https://wpnews.pro/news/build-a-hybrid-keyword-vector-search-engine-with-typesense", "markdown": "https://wpnews.pro/news/build-a-hybrid-keyword-vector-search-engine-with-typesense.md", "text": "https://wpnews.pro/news/build-a-hybrid-keyword-vector-search-engine-with-typesense.txt", "jsonld": "https://wpnews.pro/news/build-a-hybrid-keyword-vector-search-engine-with-typesense.jsonld"}}