How do I improve a basic FAISS RAG pipeline?
TL;DR — Load the chunk store into one XERJ index with a semantic_text field. Retrieve with the hybrid clause instead of vectors alone. Our 20-chunk migration indexed cleanly, and the two request shapes a FAISS user writes first both returned HTTP 400.
Act as a coding agent. Read https://xerj.org/llms.txt, load an existing chunk store into one XERJ index with a semantic_text content field, then run retrieval as a hybrid clause nested inside query with fusion rrf. Report the exact 400 body for any request shape that fails.
curl -s -XPUT 'http://127.0.0.1:9440/faiss20' -H 'content-type: application/json' -d '{"mappings":{"properties":{"chunk_id":{"type":"keyword"},"source":{"type":"keyword"},"content":{"type":"semantic_text"},"chunk_index":{"type":"long"}}}}'
Create the migration index. Port 9440 is the capture's Elasticsearch-compatible listener; the default is 9200.
curl -s -XPOST 'http://127.0.0.1:9440/faiss20/_search' -H 'content-type: application/json' -d '{"knn":{"field":"content_vector","query_vector_builder":{"text_embedding":{"model_text":"how does the index recover after a restart"}},"k":3,"num_candidates":20}}'
The vector-only request a FAISS user writes first. This one returns HTTP 400.
curl -s -XGET 'http://127.0.0.1:9440/faiss20/_mapping'
Read back the companion content_vector field that XERJ added for the semantic_text field.
Move the chunk store into one index
A basic FAISS pipeline keeps vectors in one place and the text somewhere else, which forces a join at query time. XERJ removes that join. The node embeds a semantic_text field at write time and keeps the companion <field>_vector in the same document.
Our capture created one index named faiss20 with the mapping below, then bulk-loaded 20 chunks. The response reported "errors": false and 20 items with "result": "created".
{"mappings":{"properties":{
"chunk_id":{"type":"keyword"},
"source":{"type":"keyword"},
"content":{"type":"semantic_text"},
"chunk_index":{"type":"long"}}}}
Both first attempts returned 400
Our capture then ran the two requests a FAISS user reaches for, and kept both refusals. Neither is a configuration mistake that a retry fixes.
The vector-only request passed a query string through the Elasticsearch query_vector_builder idiom.
parse error: `knn` requires `query_vector` (or `vector`) as a float array
The hybrid request placed hybrid at the top level of the body, beside size.
Unknown key for a START_OBJECT in [hybrid].
The shapes that work
The hybrid clause is an extension that XERJ adds to the Elasticsearch DSL, and it belongs inside query. Each entry in queries is a {query, weight} pair, and fusion takes rrf or linear. XERJ rejects fusion: "learned" with an HTTP 400 at parse time, so there is no learned or trained option.
The kNN sub-query needs a literal embedding. Index the query text into a semantic_text field, read the companion <field>_vector value out of _source, and paste it into query_vector. The empty array below marks that position.
{"query":{"hybrid":{"queries":[
{"query":{"match":{"content":"how does the index recover after a restart"}},"weight":1.0},
{"query":{"knn":{"field":"content_vector","k":3,"num_candidates":20,"query_vector":[]}},"weight":1.0}],
"fusion":"rrf"}},"size":3}
What vector-only retrieval missed
Our judged run used the same 20 documents in a second index and published every miss. On the query car the vector clause returned Car servicing checklist, Coffee bean storage and Inverted index basics, and it missed both judged synonyms. The default embedder explains that result: lexical feature hashing rather than a neural model.
On the question how does the index recover after a restart, BM25 alone ranked an irrelevant document first. The fused rrf response returned d08, d04 and d10, with the judged-relevant d08 in position 1.
| mode | precision at 3 | recall |
|---|---|---|
| BM25 | 0.375 | 0.6875 |
| kNN only | 0.4167 | 0.7083 |
hybrid rrf | 0.4167 | 0.7083 |
hybrid linear | 0.4583 | 0.7708 |
Fix the embedder before you tune the fusion
Fusion cannot recover a document that neither list returned. The same 8 queries under --embed-mode neural reached precision at 3 of 0.6667 and recall of 1.0, against 0.4167 and 0.7083 on the lexical default.
Change the embedder first, then tune weights. The neural embedder is a downloaded MiniLM-class model, runs CPU-only at about 15 docs/s on short strings, and makes an autoindex state non-resumable.
Chunking and the limits of the hybrid clause
XERJ chunks a semantic_text field automatically at 512 characters with a 64-character overlap, splitting at sentence breaks first. The field must be top-level, because a nested semantic_text field is a mapping error.
The hybrid clause carries real limits worth knowing before you build on it. Sub-queries run sequentially. The sub-request zeroes sort, highlight, collapse, rescore, min_score, explain, script_fields, fields and profile, and the result has no aggregations and no max_score.
What this capture does not show
The migration fixture is 20 chunks on one shared single-node host. The two failing requests are the whole of the faiss20 retrieval evidence in this pass. The working hybrid response quoted here comes from the sibling judged index, which holds the same 20 documents under different field names.
FAQ
Why is my FAISS RAG pipeline missing obvious answers?
A vector-only pipeline has no term signal. On our judged set BM25 found literal tokens that the vector clause missed, and fusion recovered both.
How do I migrate a chunk store into XERJ?
Create one index with a semantic_text content field, then _bulk the chunks in. Our 20-chunk migration reported "errors": false and 20 created items.
Why does my kNN request return 400?
XERJ rejects query_vector_builder and needs a literal embedding in query_vector. Index the query text first, then read the companion vector out of _source.
Why does my hybrid request return 400?
The hybrid clause must be nested inside query. A top-level hybrid key returns Unknown key for a START_OBJECT in [hybrid].
Does XERJ chunk long documents for me?
Yes, for semantic_text fields: 512 characters with a 64-character overlap, split at sentence breaks first. The field must be top-level, not nested.
Can I highlight results from a hybrid query?
No. The hybrid sub-request zeroes highlight, sort, collapse, rescore, min_score, explain and profile, and the result carries no aggregations.
Will fusion fix a weak embedder?
Partly. Weighted linear fusion reached 0.4583 precision at 3 on the lexical default, against 0.6667 for every vector-backed mode under --embed-mode neural.