Choosing a search engine means matching your requirements, not a favourite brand, to one of a few designs: search inside your existing database, a Lucene-based cluster (Elasticsearch, OpenSearch, Solr), a RAM-first engine (Typesense, Meilisearch), a hosted search API (Algolia) or a ranking platform (Vespa), with vector search added or built in. Whichever you pick, the database stays the source of truth and the index is a rebuildable copy.
This is part 4 of the search engines series. Part 1 mapped the kinds of search, part 2 built the inverted index and part 3 put it on disk; here we decide which engine should run it.
Picture it first: the ledger and the catalogue clerk
A cloth wholesaler keeps one thick ledger: every bolt, its price, how many are left. Customers keep asking for “red cotton for kurtas, under ₹500 a metre”.
At first the munim answers from the index pages at the back of the ledger. The answer is always current, because those pages are updated with the ledger. But as the business grows, each enquiry takes longer, and while he hunts he isn’t doing the accounts.
So the wholesaler hires a catalogue clerk with a card cabinet in the front room. For every change to the ledger, a boy runs a slip to the clerk. The clerk answers in seconds, best options first, with counts (“14 in red, 9 in maroon”). But his cards are a few minutes behind, so before selling, the munim checks the ledger. If the cabinet burns, the clerk rebuilds it from the ledger. A third option is an agency across town that answers for a fee per enquiry: nobody to manage, but you follow their rules and pay for every question.
Map it back: the ledger is your database, its index pages are PostgreSQL’s GIN index, the clerk is a search cluster, the slips are change events flowing through change data capture and Kafka, and the agency is a hosted API such as Algolia. The rule that matters most: the cabinet is a copy, rebuildable from the ledger.
The shortlist
Six families cover almost every real choice.
Inside your database. PostgreSQL indexes a tsvector with GIN (an inverted index), ranks with ts_rank and adds trigram typo matching with pg_trgm (part 1 showed the setup); MySQL has InnoDB FULLTEXT. You gain no new system and instant freshness; you pay with simple ranking and one machine’s capacity.
Lucene-based clusters. Elasticsearch, OpenSearch and Apache Solr wrap Lucene’s immutable segments (part 3) in shards, replicas, aggregations and analysers for dozens of languages. They differ mostly in licence, governance and managed offerings. OpenSearch is AWS’s fork of Elasticsearch 7.10.2, the last Apache 2.0 release. Solr’s distributed mode, SolrCloud, still relies on ZooKeeper in Solr 10.0 (March 2026), with an external ensemble recommended for production.
RAM-first engines. Typesense keeps the whole index in memory; Meilisearch keeps it in LMDB, a memory-mapped store, and is fastest when it fits in RAM. Both ship typo tolerance, facets and decent ranking in one binary.
Hosted search APIs. Algolia runs everything: you push JSON records, it serves queries. You trade control and per-request cost for zero operations.
Ranking platforms. Vespa scores every match with a cheap first-phase expression on the content nodes, re-ranks the best (100 per node by default) with a costlier model, and can re-rank again after merging. Its partial updates to in-memory fields are visible immediately, at tens of thousands per second.
Vector-first stores. pgvector and dedicated vector databases excel at nearest-neighbour search, but product search also needs exact tokens, filters and facets, so they usually serve as the semantic half of a hybrid.
The questions that decide
Work through these before naming a product; each one rules something out.
- Corpus size and growth. Thousands of rows fit anywhere; tens of millions decide whether you must shard and whether the index must fit in RAM. Plan for the size in two years; migrating later can take a quarter.
- Query types. Stemmed keywords work everywhere. Typos, Hinglish, synonyms, prefixes, substrings, geo and facet counts (part 1) separate engines quickly.
- Freshness. Inside the database, a committed row is searchable at once. A separate engine trails by the pipeline lag plus its refresh, one second by default in Elasticsearch. Algolia and Meilisearch apply writes as asynchronous tasks, which Algolia says usually finish in milliseconds or seconds.
- Relevance tuning. Field weights and a popularity boost are easy anywhere. Learning-to-rank, merchandising rules, A/B tests and per-query models need a Lucene engine, Vespa, or a SaaS that bundles the tooling.
- Facets and analytics. Counting by brand and size over 200,000 matches needs columnar doc values. Lucene engines are built for it and double as log-analytics stores.
- Vectors and hybrid. Most engines store vectors now; they differ in memory per vector, quantisation, filtering and how lexical and vector lists are fused.
- Multi-tenancy. Prefer a shared index with a tenant filter. Elasticsearch allows 1,000 non-frozen shards per node and wants 1 GB of master heap per 3,000 indices, so 50,000 tenant indices need at least 50 nodes and about 17 GB of master heap. Typesense scoped keys, Meilisearch tenant tokens and Algolia secured API keys embed the tenant filter in the key.
- Who runs it. Self-hosting means on-call, upgrades and JVM tuning; a managed cluster removes the servers but not the sizing; a SaaS removes both.
- What the bill grows with. RAM for in-memory engines, disk and page cache for Lucene, instance-hours on managed services, records and requests on a SaaS.
- Licence. Running any engine behind your own app is fine. Reselling search as a service, shipping the engine inside a product, or an “OSI-approved only” policy narrows the list (dates below).
- Team skills. A team fluent in Postgres runs Postgres search better than a neglected Elasticsearch cluster. Relevance tuning is a skill too: without an owner, a powerful engine just ranks badly, faster.
Benchmark with your own queries
Vendors publish fast numbers on someone else’s data. Load a realistic slice of your corpus, replay a day of real queries (long tail and typos included), and measure p99 latency, index size, RAM and the zero-result rate before you commit.
Licences and governance, with dates
Search engines have had one of the most eventful licence histories in infrastructure, and interviewers sometimes probe it.
- January 2021. Elastic moved Elasticsearch and Kibana source code from Apache 2.0 to a choice of SSPL or the Elastic License before 7.11, to stop cloud providers offering them as a service without contributing back.
- 2021. AWS forked 7.10.2 as OpenSearch under Apache 2.0; version 1.0 was production-ready in July 2021. Amazon OpenSearch Service still runs legacy Elasticsearch only up to 7.10.
- 29 August 2024. Elastic added AGPLv3, an OSI-approved licence, as a third option ahead of 8.16; its default distribution stays under ELv2.
- 16 September 2024. AWS moved OpenSearch to the new OpenSearch Software Foundation under the Linux Foundation, with AWS, SAP and Uber as premier members.
As of October 2026, Solr and Vespa are Apache 2.0, Typesense is GPLv3, Meilisearch Community Edition is MIT while its Enterprise Edition (which adds sharding) is BUSL 1.1 or commercial, PostgreSQL has its own permissive licence, and Algolia is proprietary. Elastic’s FAQ says that if you run Elasticsearch behind your own application, nothing changed for you; licences bite when you resell search, ship the engine, or need one cloud’s managed service. Some Elastic features also sit in paid tiers, so check the tier of anything you design around.
The standard architecture
Whatever engine you choose, the shape around it is the same, and it is what interviewers care about most.
The database is the source of truth. Sellers, admins and the order service write only there; the index never takes user writes.
Changes leave through the log. A CDC tool such as Debezium reads PostgreSQL’s logical replication stream or MySQL’s binlog and publishes each change to Kafka. To publish shaped events (“product published”) instead of raw rows, write them to an outbox table in the same transaction: the transactional outbox. Either way, a change reaches the log if and only if it committed.
The indexer is idempotent and versioned. It enriches each product (category names, seller rating, 30-day sales) and writes in bulk, passing the row’s version or log position as an external version. Elasticsearch rejects a write whose version is not higher than the stored one, so a replayed or late event never overwrites newer data.
The index is rebuildable. Build products_v2 from a fresh snapshot, replay Kafka from where the snapshot began, then repoint the products alias. That is how you change analysers or embedding models and recover from corruption; part 7 has the details.
Read from the index, confirm from the database. The index finds and ranks; the API hydrates the top 20 with live price and stock, checkout re-checks, and sellers see their own edits from the database (read-your-writes).
Don’t dual-write
“Write to Postgres, then to Elasticsearch” from the application looks simpler, but a crash or timeout between the two writes leaves them disagreeing forever, and two concurrent updates can reach the index in the wrong order. Feed the index from what actually committed, through CDC or an outbox.
Start with Postgres until…
If your data already lives in PostgreSQL, its full-text search is the right first engine: no new system, no pipeline, and the GIN index commits with the row. The question is when it stops being enough.
Broad queries match too many rows. The ranking documentation is blunt: ranking “requires consulting the tsvector of each matching document, which can be I/O bound and therefore slow”, and that is “almost impossible to avoid”. The GIN lookup is fast; ordering the matches is not, so cost grows with matches, not with the 20 results you show.
Redo the arithmetic for your corpus: cost ≈ matching rows × tsvector size × queries per second. At about 1 KB per tsvector, a head query such as “kurta” matching 200,000 rows reads 200 MB; at 300 queries per second that is 60 GB/s. Lucene engines avoid most of this work: per-block maximum scores (block-max WAND) let them skip whole blocks of postings that cannot reach the top 20, as part 2 explains.
Relevance becomes a product. ts_rank and ts_rank_cd “do not use any global information”: no inverse document frequency, so a rare word counts no more than a common one, and only four field weights, A to D. Daily synonym changes, merchandising rules, learning-to-rank and ranker A/B tests are search-engine features.
Many facet counts per query. Each facet is an aggregation over the whole match set; five facets over 200,000 matches is real work on a row store.
Typos, transliteration, synonyms. pg_trgm lives in a separate index and is awkward to blend with ts_rank. Synonym dictionaries are files on the server. There is no built-in transliteration or spelling correction: turning “panir” into “paneer” is your code’s job.
Search must scale apart from the database. Replicas buy time, but you want to add capacity for a sale without resizing your system of record.
Long documents and big vectors. A tsvector must stay under 1 MB, with positions up to 16,383, so long manuals need chunking. pgvector stores a vector in 4 × dimensions + 8 bytes, indexes up to 2,000 dimensions, and scales vertically, with replicas or with Citus sharding.
Stretch Postgres before leaving: search on a replica, cap ranking to the most popular few thousand matches (trading recall), or try a BM25 extension such as ParadeDB’s pg_search, a Tantivy index inside Postgres under AGPLv3 (check that your managed provider allows it). The rule: Postgres full-text search is enough while the table and its GIN index fit in RAM, head queries match under about 10,000 rows, you need a handful of facets and stemming-level linguistics, and search fits on a replica or two.
And MySQL?
InnoDB FULLTEXT ranks with a TF-IDF variant that MySQL says is based on BM25 and TF-IDF, and updates the index at commit. By default it ignores words under three characters, so “XL” or the brand “HP” find nothing until you lower innodb_ft_min_token_size, restart and rebuild. It is not supported on partitioned tables, and % is not a full-text wildcard. Fine for a blog; for product search its limits arrive sooner than PostgreSQL’s.
Where the engine runs
Picking a product and picking who operates it are separate decisions: the same Elasticsearch can be your own Kubernetes cluster or a serverless project you never see.
Amazon OpenSearch Service bills domains per instance-hour plus storage, and serverless collections in OpenSearch Compute Units. Elastic Cloud Serverless separates indexing from search and is versionless, but drops cluster-level controls such as custom plugins and user-initiated snapshots. Typesense, Meilisearch and Vespa each run a hosted cloud. On a SaaS, model the bill first: Algolia’s pay-as-you-go plans charge per thousand search requests and per thousand records beyond an allowance (October 2026), and if your UI queries on every keystroke, “kurta” is five requests.
The big comparison
| Engine | Scales by | Relevance tuning | Facets, analytics | Vectors, hybrid | Freshness | Ops burden | Licence |
|---|---|---|---|---|---|---|---|
PostgreSQL FTS + pg_trgm |
One primary plus replicas | ts_rank, A–D weights, no IDF |
SQL over matches | pgvector; fuse in SQL | Same transaction | None extra | PostgreSQL (permissive) |
MySQL InnoDB FULLTEXT |
One primary plus replicas | TF-IDF variant, few knobs | SQL | Another store | At commit | None extra | GPLv2 (Community) |
| Elasticsearch | Shards and replicas, many nodes | BM25, function scores, LTR | Rich aggregations | Quantised HNSW, RRF | ~1 s refresh + pipeline | High: JVM, shards | ELv2, SSPL or AGPLv3 |
| OpenSearch | Same model (7.10.2 fork) | BM25, LTR plugin | Rich aggregations | k-NN plugin, hybrid pipeline | ~1 s refresh + pipeline | High | Apache 2.0, Linux Foundation |
| Apache Solr | SolrCloud shards + ZooKeeper | BM25, function queries, LTR | Mature faceting | Dense vectors, quantised in 10.0 | Near real time | High, plus ZooKeeper | Apache 2.0 |
| Typesense | Vertical; full copy per node | Typo-aware, field weights, pinning | Built in | Built in, hybrid | Near real time | Low | GPLv3 |
| Meilisearch | One node (sharding in Enterprise) | Ordered ranking rules | Built in | Hybrid via embedders | Async task queue | Low | MIT; Enterprise BUSL 1.1 |
| Algolia | Vendor’s problem; 100 GB index cap | Ranking formula, rules, A/B tests | Built in | NeuralSearch on top plan | Async, ms to seconds | None | Proprietary SaaS |
| Vespa | Content clusters redistribute data | Ranking expressions, tensors, ONNX, GBDT | Grouping | Native tensors, HNSW | Partial updates at once | High, steep curve | Apache 2.0 |
| pgvector / vector DB | Varies (pgvector: one node) | Distance only | Filters | The whole point | Varies | Low to medium | Varies |
Read it by elimination. Logs and analytics point to the Lucene engines. A small team with a few million records fits Typesense, Meilisearch or Algolia. ML ranking over many candidates at high traffic is Vespa’s home ground. For all of that at tens of millions of documents it is Elasticsearch or OpenSearch, and licence, cloud and team familiarity break the tie.
A decision tree
The order matters: workload first (logs are a different problem), then whether you need a new system at all, then who runs it, then whether one machine can hold the index. Only then compare ranking features.
Worked example: Bazaar at four sizes
Assumptions you can change: a product is about 2 KB of JSON, about 500 bytes of it searchable, a tsvector is about 1 KB, and embeddings have 768 dimensions. Index-to-raw ratios vary wildly (Meilisearch’s own sample turned 9.1 MB of JSON into 224 MB on disk), so where an engine publishes no ratio I assume 1–1.5× the raw JSON. Measure yours.
50,000 products: PostgreSQL
Rows take 50,000 × 2 KB = 100 MB and tsvectors 50 MB, so everything fits in the smallest database instance’s RAM. A broad query matching 5% ranks 2,500 rows, about 2.5 MB of tsvector: a few milliseconds. Use a generated tsvector (title in weight A, brand and category in B), GIN, pg_trgm for “kurtha” and a query-rewrite table for Hinglish. No pipeline, no lag, no new bill.
5 million products: a dedicated engine
Now “kurta” matches about 4%, 200,000 rows: 200 MB of tsvector per query, 60 GB/s at a 300-queries-per-second peak. Shoppers also want facet counts, typo tolerance and merchandised results. Three good options:
- Typesense. Searchable data is 5M × 500 B = 2.5 GB, so 2–3× that is 5–7.5 GB of RAM per node, and a 3-node cluster keeps a full copy on every node. Typesense’s own data point is close: a 3-node, 8-vCPU cluster served 250 concurrent queries per second over 3 million records.
- Algolia. 2 KB records fit even the smallest plan’s 10 KB record limit, and 10 GB is under the 100 GB index cap. An average of 100 queries per second is 100 × 86,400 × 30 ≈ 259 million requests a month; price that before signing.
- Managed Elasticsearch or OpenSearch. About 10–15 GB of index per copy: one or two primaries within Elastic’s 10–50 GB shard guidance, plus a replica, on three small nodes.
Growth decides. If 50 million products and vectors are two years away, go straight to Elasticsearch or OpenSearch and skip a second migration; otherwise Typesense is simpler and cheaper.
50 million products with hybrid search: a Lucene cluster
Text is 50M × 2 KB = 100 GB of JSON, so 100–150 GB of index per copy: four primaries of 25–38 GB, each holding 12.5 million documents, well under the 200 million guidance.
Vectors separate the engines. Raw float32 embeddings are 50M × 768 × 4 B = 153.6 GB. By Elastic’s formulas, each copy needs in page cache:
- int8 HNSW: (768 + 16) B × 50M = 39.2 GB, plus a graph of 50M × 4 × 16 = 3.2 GB: 42.4 GB.
- BBQ (binary) HNSW: (⌈768 ÷ 64⌉ × 8 + 14) = 110 B × 50M = 5.5 GB, plus 3.2 GB of graph: 8.7 GB, with oversampling and rescoring to recover accuracy.
Now eliminate. Typesense needs 50–75 GB for text plus 7 B × 768 × 50M = 268.8 GB for vectors, roughly 320–345 GB on every node. pgvector needs about 154 GB for the vector column alone (77 GB as halfvec) plus its HNSW index, beside a 100 GB catalogue, on one primary. Algolia would sit right at its 100 GB index cap and keeps semantic search on its top plan. That leaves Elasticsearch or OpenSearch with quantised HNSW and rank fusion (Elasticsearch’s RRF retriever, OpenSearch’s hybrid search pipeline), adding replicas until a benchmark meets the sale-day peak. Vespa is the strong alternative if Bazaar wants learned ranking on every query and live price and stock updates.
Logs: a different problem
Bazaar’s 400 services write about 1 TB of logs a day: 11.6 MB/s, about 23,000 lines per second at 500 bytes each. Queries look like “checkout errors in the last 15 minutes containing order_id=48213”: recall and recency matter, relevance hardly at all.
Assume the index is about the raw size. Seven days hot is 7 TB, 14 TB with a replica. At 50 GB per shard that is 20 primaries a day and 280 shards for the week with replicas, inside the 1,000-per-node limit even on a few nodes. Use data streams with rollover so expiring a day drops an index instead of deleting 2 billion documents. The cheaper alternative is Grafana Loki, which indexes only labels and stores compressed chunks in object storage: cheaper to ingest and store, slower to grep across wide time ranges. Algolia’s 100 GB cap would fill in about 2.4 hours (1 TB a day is 41.7 GB an hour), and an in-memory engine would need terabytes of RAM.
In the interview
When you name an engine, give the number that forced it: “Typesense would need about 345 GB of RAM on every node at 50 million products with vectors; Elasticsearch with int8 HNSW needs about 42 GB of page cache per copy for the vectors, so I’d pick Elasticsearch.” One line of arithmetic beats a list of features.
Going deeper: traps when choosing
Choosing for today’s corpus. Write down the size in two years and the roadmap (vectors, analytics, regions) first; a setup that is perfect at 5 million products can be the wrong shape at 50 million.
Making the index the only copy. Search engines are near-real-time copies without multi-document transactions. Once some data lives only in the index, “rebuild from source” stops working.
Underestimating migration. New analysers, re-tuned relevance and query-by-query comparison of results take weeks. Feed both indexes from the same Kafka topic and switch behind a flag.
Hard limits found in production. Meilisearch ignores query words after the tenth, indexes at most 65,535 positions per attribute and answers 1,000 concurrent searches before returning 503s. Algolia allows 65 searchable attributes and facets per index; Elasticsearch caps vectors at 4,096 dimensions. Read every candidate’s limits page before the design review.
Hot fields in the text index. Stock and price change far more often than titles. A GIN update touches many keys, which is why GIN has a pending list (4 MB by default) that searches must also scan; in Lucene, an update is a delete plus a re-add. Keep hot numeric fields out of the full-text document, or use an engine like Vespa that updates them in place.
Cheat sheet
| Situation | Reach for | Because |
|---|---|---|
| Up to a few million rows, data already in Postgres | PostgreSQL FTS + pg_trgm |
No pipeline, transactional, cheap |
| Head queries match 100k+ rows, or many facets | A dedicated engine | Ranking reads every match’s tsvector |
| Small team, index fits in RAM, typo tolerance | Typesense or Meilisearch | Good defaults, one binary |
| No ops wanted, records ≤ 10–100 KB | Algolia | Fully hosted; billed by records and requests |
| Tens of millions of docs, facets, hybrid | Elasticsearch or OpenSearch | Shards, aggregations, quantised HNSW |
| ML ranking per query, live attribute updates | Vespa | Phased ranking on content nodes |
| Logs, TBs a day | ES/OpenSearch data streams, or Loki | Time-based indices; label-only index |
| Permissive licence required | OpenSearch, Solr, Vespa (Apache 2.0); Meilisearch CE (MIT) | Elasticsearch’s AGPLv3 and Typesense’s GPLv3 are copyleft |
| Architecture, always | DB → CDC/outbox → Kafka → indexer → index | The index is a rebuildable copy |
| Reads | Index for IDs, database for price and stock | The index lags by a second or more |
How to say it in the interview
“I’d keep Postgres as the source of truth and treat search as a rebuildable copy: CDC into Kafka, an indexer that writes with the row version as an external version, and a search API that takes the top 20 IDs from the index and hydrates price and stock from the database. At 50,000 products I’d use Postgres full-text search with trigram fuzziness. At 5 million, broad queries match about 200,000 rows, 200 MB of tsvector per query, so I’d move to a dedicated engine. At 50 million with vectors I’d pick Elasticsearch or OpenSearch: four primary shards, and int8 HNSW at about 42 GB of page cache per copy, where an in-memory engine would need over 300 GB on every node.”
Elasticsearch or OpenSearch? Functionally close for most product search. OpenSearch if you want Apache 2.0 under a Linux Foundation project or Amazon’s managed service; Elasticsearch for Elastic’s newest features and Elastic Cloud. Since 2024 Elasticsearch also offers AGPLv3, so “not open source” no longer decides it.
Why not make Elasticsearch the primary store? It is near-real-time, has no multi-document transactions, and mapping or analyser changes need a reindex. If it is the only copy, a bad mapping or a corrupt index is data loss. Keep the truth in the database.
How stale can results be, and what do you do about it? CDC lag plus indexer batching plus about a 1-second refresh: usually seconds, more during a backlog. Hydrate price and stock from the database, re-check at checkout, show sellers their edits from the database, and alert on consumer lag.
When would you choose Algolia? When nobody can operate or tune a cluster, the corpus fits the plan’s record and index limits, and the request-based bill at expected traffic, keystrokes and bots included, costs less than engineers’ time.
What about a dedicated vector database? Right for pure semantic retrieval at very large scale, but product search still needs exact tokens, filters and facets. If the lexical engine already supports quantised HNSW and fusion, one system beats two indexes to keep in sync.
Where to go next
- Next in the series: Elasticsearch in depth, the cluster, shards, replication and search path of the engine most designs end up with.
- Elasticsearch and PostgreSQL: what interviewers expect you to know about each.
- Change data capture, Transactional outbox and Kafka: the pipeline that feeds the index.
- Vector databases and Embeddings and semantic search: the semantic half of hybrid search.
- Search end to end: the whole Bazaar system, from ingest to the ranking funnel.
- Design product search for an online store and Design a distributed logging system: these choices in an interview.
- Multi-tenant data and Cost-aware design: tenancy and cost trade-offs in general.
References and further reading
- PostgreSQL 18 documentation: Controlling text search (ranking search results)postgresql.orgts_rank and ts_rank_cd, the A–D weights, why ranking uses no global statistics, and why ranking every match is I/O bound.
- PostgreSQL 18 documentation: GIN indexespostgresql.orgWhy inverted-index updates are slow, the fast-update pending list (gin_pending_list_limit, 4 MB default) and its effect on search latency.
- MySQL 8.4 Reference Manual: Natural language full-text searchesdev.mysql.comInnoDB’s three-character minimum token size, stopwords, collection-wide term weighting and MyISAM’s 50% threshold.
- Elastic: Licensing FAQelastic.coThe 2021 move from Apache 2.0 to SSPL and ELv2 (before 7.11) and the 2024 addition of AGPLv3 (ahead of 8.16).
- Linux Foundation: OpenSearch Software Foundation launch (16 September 2024)linuxfoundation.orgAWS transferring OpenSearch to a vendor-neutral foundation under the Linux Foundation, and its founding members.
- Typesense documentation: System requirementstypesense.orgIn-memory index sized at 2–3× the searchable fields, 7 bytes per vector dimension, Raft clusters of 3 or 5 nodes and published throughput data points.
- Meilisearch documentation: Known limitationsmeilisearch.comHard limits from LMDB and design choices: 10 query words, 65,535 positions per attribute, 2 TiB recommended index size, 1,000 concurrent searches.
- Algolia documentation: Service limitsalgolia.comRecord size of 10–100 KB by plan, 100 GB application and index size, and other per-plan caps of a hosted search API.
- Elasticsearch documentation: Tune approximate kNN searchelastic.coBytes per vector for int8 and BBQ quantisation, HNSW graph memory (num_vectors × 4 × m) and why vectors must stay in the page cache.
- pgvector READMEgithub.comHNSW and IVFFlat indexes, 2,000-dimension index limit for vector, storage of 4 × dimensions + 8 bytes, post-filtering and scaling advice.