Skip to content
曾亮懿 ZengLiangYi Blog

Semantic Search: Candidate Deduplication and Graph Context

Trace query vectors, candidate growth, lexical boosts, and relation expansion in ChatCrystal, distinguishing list filtering from semantic retrieval.

曾亮懿

曾亮懿

3 min read
Table of contents Show Hide

Five returned notes do not mean five vector candidates, and their scores are not necessarily unadjusted cosine similarities. Explain missed results and ordering by separating application stages from API fields.

This article follows the ChatCrystal embedding service at 7642768. Constants describe that version’s rules, not universally optimal retrieval parameters.

Two entrances serve different questions

/api/notes?search=... applies SQL LIKE to titles and summaries, useful for checking existing terms. /api/search?q=... embeds a query and retrieves note candidates. LIKE also has wildcard semantics; arbitrary input is not always literal matching.

CLI examples on the default local connection:

crystal --json notes list --search SQLite --limit 5
crystal --json search "SQLite concurrent writes" --limit 5

The first JSON response is a list object containing items; the second is a candidate array. The JSON branch forwards the limit instead of using the TTY search interface’s own fixed count. Eligible results can still be fewer than the upper bound.

From note text to candidates

Notes are embedded from selected fields; queries use the current embedding configuration. Both need matching settings. Follow the embedding configuration guide when changing models. Paragraph grouping produces multiple chunks, so a note can have several vectors.

Vectra v0.15.0 LocalIndex scores candidates and retains top K. This path does not use HNSW or LSH approximate candidate lookup. Evaluate cost and quality against actual library size and models; a local index does not establish zero cost or fixed response time.

Database checks, deduplication, and ranking

The application reads database chunks corresponding to candidates and requires note status done. An index entry alone does not establish an eligible result. Multiple chunks are deduplicated by note, keeping the higher score.

Term processing includes ASCII technical words and Han-character bigrams. A candidate containing every extracted term receives a score floor of 0.82; otherwise candidates below 0.58 are filtered. Boosting occurs after materializing vector candidates. It is not a separate keyword fallback scanning the entire library.

If checks, deduplication, or filtering leave too few notes, the code progressively grows candidate count, bounded by available index count or stopping conditions. This can add candidates without guaranteeing the requested result count or inclusion of the semantically correct note.

Compare relation expansion through the API

Use --data-urlencode instead of manually assembling queries with special characters:

curl --get http://localhost:3721/api/search \
  --data-urlencode 'q=SQLite concurrent writes' \
  --data-urlencode 'limit=5'

Optionally enable relation expansion:

curl --get http://localhost:3721/api/search \
  --data-urlencode 'q=SQLite concurrent writes' \
  --data-urlencode 'limit=5' \
  --data-urlencode 'expand=true'

Current expansion follows one-hop relations from direct hits, requiring confidence at least 0.5. Linked scores use direct score × 0.7 × confidence. This is not multihop path search. Notes already in the result set are skipped, so it does not choose the strongest path for every linked result. Expansion can produce more notes than the direct-hit limit.

Interpret response fields correctly

The HTTP route wraps the array in data, returning note_id, conversation_id, title, project, score, tags, and via_relation. The last field marks a relation type, not a complete path or proof of correct source content. CLI arrays are unwrapped and do not require .data[].

Read the body for note_id, then check the original conversation and current task. Compare the two entrances and expansion switch using known-answer, paraphrase, exact-term, and no-answer queries. Record false matches instead of showing only successful examples. See debugging-solution recovery for reuse and RAG and knowledge graphs for evidence boundaries.

Share:

Related Posts

Recover a debugging solution: retrieve, read, then verify reuse

Use a constructed debugging scenario to check notes, vectors, and search results, then compare the original conversation with the current project.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Semantic search #Debugging

Semantic Search and Context: Finding Hidden Code Dependencies

Combine vector search with one-hop graph relation expansion in code refactoring, keeping similarity scores, candidates, and sources verifiable on disk.

曾亮懿 曾亮懿
3 min read
#ChatCrystal #Semantic search #Knowledge graph

From RAG to Knowledge Graphs: Preserving Context in Retrieval

Compare keyword search, vectors, and knowledge graphs. Trace one-hop candidate expansion in ChatCrystal and identify boundaries relationships cannot prove.

曾亮懿 曾亮懿
5 min read
#ChatCrystal #RAG #Knowledge graphs #Retrieval

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.