This episode dissects the ChromaDB query API: query_texts and query_embeddings, the n_results setting, and the include parameter for choosing which of documents, metadatas, distances, or embeddings are returned, complete with the distance vs similarity interpretation.

In episode 4 you could store and manage data. Now we move into the part that makes ChromaDB special: querying and semantic search. This is where you ask in natural language and ChromaDB finds documents that mean the same thing, not just ones that look the same.
Episode 5 dissects the query API thoroughly: two ways to provide input (query_texts and query_embeddings), how many results to return (n_results), what gets returned (include), and how to read the results — especially the often-confusing difference between distance and similarity.
The most natural way: provide text and let ChromaDB embed it itself:
hasil = collection.query(
query_texts=["Bagaimana cara deploy aplikasi?"],
n_results=3,
)collection.query(query_texts=["..."], n_results=3) embeds the query text with the collection's embedding function, searches for the 3 nearest vectors, then returns the results. This is the main gateway to semantic search.
If the query embedding is already generated outside — for example, from the same model used for the data — use query_embeddings:
hasil = collection.query(
query_embeddings=[[0.2, 0.8, 0.4, 0.6]],
n_results=2,
)The only difference between the two is the embed step. collection.query(query_embeddings=[[...]]) uses the vectors you provide directly. For RAG pipelines that embed queries with a dedicated model, this form is more efficient because it saves one call.
By default, query returns ids, documents, metadatas, and distances. You can control what is returned via include:
hasil = collection.query(
query_texts=["Apa itu Kubernetes?"],
n_results=2,
include=["documents", "metadatas", "distances"],
)The available options for include=["documents", "metadatas", "distances"]:
documents: the source text, required for RAG.metadatas: attributes like source and date.distances: the distance values of results from the query.embeddings: the result vectors, useful for advanced analysis.The more you request, the bigger the returned payload. In production, request only what you need to save bandwidth.
The most commonly misunderstood concept: ChromaDB returns distance, not similarity. The smaller the distance, the more similar the result:
hasil = collection.query(query_texts=["deploy aplikasi"], n_results=2)
for i, dok in enumerate(hasil["documents"][0]):
jarak = hasil["distances"][0][i]
print(f"{dok} -> jarak {jarak:.4f}")In the code above, hasil["documents"][0] is the list of results for the first query, and hasil["distances"][0] is each one's distance. Results are ordered from smallest distance (most relevant) to largest.
Distance depends on the metric in use (episode 9). For cosine distance, similarity is computed as:
similarity = 1.0 - jarakThe formula similarity = 1.0 - jarak applies to cosine. So a distance of 0.15 means a similarity of 0.85. Remember the inverse when comparing results between collections with different metrics.
Warning
Do not compare distance values between collections that use different metrics. Cosine, L2, and inner product produce completely different distance scales — episode 9 explains when to use which.
query_texts accepts a list, so a single call can answer many questions:
hasil = collection.query(
query_texts=["Apa itu Docker?", "Apa itu Kubernetes?", "Apa itu CI/CD?"],
n_results=1,
)
for i, dok in enumerate(hasil["documents"]):
print(f"Q{i}: {dok[0]}")The result structure is always two levels: hasil["documents"][i] is the list of results for the i-th query. Batch querying collection.query(query_texts=[...]) cuts latency because one round-trip handles many questions — a pattern we will use again for multi-query in episode 19.
If a query returns irrelevant documents, a few possibilities: the collection's embedding function differs from the one used to generate the data, or the document chunk size (episode 10) is too large so meanings get mixed. Check the embedding function first, then your chunking strategy.
All list arguments in add, query, and others must have equal lengths. This error usually comes from a query_texts that is not a list:
hasil = collection.query(query_texts="deploy")
hasil = collection.query(query_texts=["deploy"])Always wrap a single query in a list. collection.query(query_texts=["deploy"]) is the correct form — this is the most common mistake in community forums.
Episode 5 unlocked ChromaDB's core power: querying and semantic search. You can now query in text or vector form, control the number and contents of results via n_results and include, read results ordered from smallest distance, and avoid the distance-versus-similarity trap.
Key takeaways:
query_texts for text queries; query_embeddings for vector queries.n_results sets the number of results; include sets what they contain.1 - distance.In the next episode, episode 6, we will discuss embeddings and embedding functions — the built-in ONNX MiniLM model that runs locally, how to switch to sentence-transformers or OpenAI, pre-computed embedding integration, and how to write a custom embedding function. This determines the overall quality of your semantic search.