# Retrieval OpenViking provides multiple retrieval methods, including simple vector similarity search, keyword search, intelligent retrieval with session context, regex pattern matching, and file pattern matching. ## find vs search | Aspect | find | search | |--------|------|--------| | Intent Analysis | No | Requires session content and `retrieval.enable_intent=true` | | Session Context | No | Optional | | Query Expansion | No | When session content exists and intent analysis is enabled | | Reranking | No | Used for text queries when a reranker is configured | | Default Limit | 10 | 10 per planned query | | Use Case | Simple queries | Conversational search | ## Retrieval Pipeline The core retrieval pipeline is as follows: ``` Query → Intent Analysis (search only, optional) → Global Vector Search → Rerank (search only, optional) → Results ``` 1. **Intent Analysis** (search only): Understand query intent, expand queries 2. **Vector Search**: Find candidates using embeddings 3. **Rerank**: THINKING with a usable reranker reranks `2 × limit` recalled candidates once; otherwise recall is limited to `limit` hits 4. **Results**: Return top-k contexts ## API Reference ### find() Semantic retrieval without session context. #### 1. API Implementation Introduction The `find()` method runs one global vector similarity search in QUICK mode for simple query scenarios. It recalls `limit` candidates and supports filtering by L0/L1/L2 through `level`. **Processing Pipeline**: 1. Convert query text to vector 2. Perform global vector search within specified target URI 3. Apply the score threshold and return matched contexts without reranking **Code Entry Points**: - `sdk/python/openviking_sdk/client.py:SyncHTTPClient.find()` - Python SDK entry (HTTP) - `openviking/retrieve/hierarchical_retriever.py:HierarchicalRetriever.retrieve()` - Core retrieval implementation - `openviking/server/routers/search.py:find()` - HTTP router - `crates/ov_cli/src/commands/search.rs:find()` - Rust CLI command #### 2. Interface and Parameter Description **Parameters** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | query | str | No | "" | Search query string. May be omitted for an image query or a filter-only lookup | | image_url | str or list[str] | No | None | One image URI or an ordered list of image URIs, each as a `data:image/...;base64,...`, `http(s)://`, or `viking://` URI. Requires a multimodal embedding model | | target_uri | str \| List[str] | No | "" | Limit search to specific URI prefix | | events_time_decay_protection | str \| null | No | null | Omit or pass `null` to disable decay. Pass `"0"` to decay immediately, or a duration such as `"7d"` to preserve the original score during that period and decay afterward. Supports non-negative integer `Xm`/`Xh`/`Xd` | | context_type | str \| List[str] | No | None | Limit results to one or more `ContextType` values: `memory`, `resource`, or `skill` | | tags | List[str] | No | None | Explicit retrieval tags in strict `k=v` form. Multiple tags are combined with AND; a result must contain every requested tag | | limit | int | No | 10 | Maximum returned results | | node_limit | int | No | None | Optional HTTP alias; overrides `limit` when provided | | score_threshold | float | No | None | Minimum relevance score threshold | | filter | Dict | No | None | Metadata filter | | since | str | No | None | Lower time bound, accepts `2h` or ISO 8601 / `YYYY-MM-DD`. Timezone-less values are interpreted as UTC. CLI `--after` maps to this field | | until | str | No | None | Upper time bound, accepts `30m` or ISO 8601 / `YYYY-MM-DD`. Timezone-less values are interpreted as UTC. CLI `--before` maps to this field | | time_field | "updated_at" \| "created_at" | No | "updated_at" | Metadata time field used by `since` / `until` | | level | int \| str \| List[int] | No | None | Limit results to specific level(s), e.g., `0`, `1`, `2`, or `0,1,2`. CLI `--level`/`-L` maps to this field | | include_provenance | bool | No | False | Include provenance/query-plan details in serialized result | | read_content | bool | No | False | Read each final matched URI with the visible-content read semantics and inline the result as `content`. Individual read failures leave the hit unchanged. | | telemetry | bool \| object | No | False | Attach telemetry data to response | **Target resolution notes**: - With empty `target_uri`, non-ROOT retrieval searches the current user root (`viking://user/{user}`) and shared `viking://resources`. - To filter the current user's peer collection to one peer for filesystem and retrieval operations, send `X-OpenViking-Actor-Peer: ` or construct the SDK/CLI client with `actor_peer_id`. See [Multi-Tenant: Peer Collection Filter](../concepts/11-multi-tenant.md#peer-restricted-view). - Home-alias target URIs such as `viking://~/memories`, `viking://~/resources`, and `viking://~/skills` are expanded to the canonical path from the authenticated request identity. The uid-less spelling `viking://user/memories` (and the same shape for `resources`, `skills`, `peers`, `privacy`, `sessions`) is rejected with an error pointing at the `viking://~/...` form. **Image search notes**: - Image queries use the image vector as the query and search L2 resource leaf nodes in the target scope by default. Results are not limited to image files; multimodal embedding decides similarity between the query image and text/image resources. - Text-only embedding models still index image summaries, but image query input is rejected. - Existing image resources keep their existing vectors; image-vector recall applies to images vectorized after this capability is enabled or after a later reindex. **FindResult Structure** When `read_content=true`, each successfully read hit additionally contains `content`. This is a full file read, so use `limit` to bound response size. `search(mode="context")` rejects `read_content` because context assembly already enforces its own token budget. ```python class FindResult: memories: List[MatchedContext] # Memory contexts resources: List[MatchedContext] # Resource contexts skills: List[MatchedContext] # Skill contexts query_plan: Optional[QueryPlan] # Query plan (search only) query_results: Optional[List[QueryResult]] # Detailed results total: int # Total count (auto-calculated) ``` **MatchedContext Structure** ```python class MatchedContext: uri: str # Viking URI context_type: ContextType # "resource", "memory", or "skill" level: int # Tier (0=L0, 1=L1, 2=L2) abstract: str # L0 content overview: Optional[str] # L1 overview (optional for non-leaf nodes) category: str # Category score: float # Relevance score; scale depends on the retrieval pipeline match_reason: str # Why this matched ``` `score` is not universally bounded to `[0, 1]`: its scale depends on the backend, metric and postprocessing. See [observer.vikingdb](18-observer.md#observer-vikingdb) for the runtime metric, pure-dense scale and its scope. #### 3. Usage Examples **HTTP API** ``` POST /api/v1/search/find ``` ```bash curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "how to authenticate users", "limit": 10 }' ``` **Search with Target URI and Time Filter** ```bash curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "authentication", "target_uri": "viking://resources", "since": "7d", "time_field": "created_at" }' ``` **Search by Context Type** ```bash curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "authentication", "context_type": ["memory", "resource"] }' ``` **Image Search** ```bash curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "image_url": "viking://resources/images/cat.png", "limit": 10 }' ``` **Search by Explicit Retrieval Tags** ```bash curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "rollback runbook", "tags": ["env=prod", "team=search"] }' ``` Tags must use strict `k=v` strings. When multiple tags are provided, `find()` requires all of them; the example above only returns contexts whose explicit retrieval tags contain both `env=prod` and `team=search`. Legacy invalid stored tags are discarded when a full index record is written or an explicit tag field is updated. Reads filter invalid tags without changing storage. To repair a known affected file or directory without re-embedding or replacing its valid tags, call `POST /api/v1/content/set_tags` with `{"uri": "", "tags": [], "mode": "append"}`. For directories, add `"recursive": true` only when descendants should also be repaired. Bare values such as `default` are removed, not assigned an inferred key. **Python SDK** ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Basic search results = client.find(query="how to authenticate users") # Search with filter and time range recent_emails = client.find( query="invoice", target_uri="viking://resources/email", options={ "since": "7d", "time_field": "created_at", }, ) # Search only memories and resources typed_results = client.find( query="authentication", options={"context_type": ["memory", "resource"]}, ) # Search by local image, bytes, data URI, HTTP URL, or viking:// URI image_results = client.find(query="", image="/path/to/photo.png") # Search by explicit retrieval tags. Multiple tags are AND-ed. tagged_results = client.find( query="rollback runbook", options={"tags": ["env=prod", "team=search"]}, ) # Iterate through results for context in results.get("resources", []): print(f"URI: {context['uri']}") print(f"Score: {context.get('score', 0.0):.3f}") print(f"Type: {context.get('context_type')}") print(f"Abstract: {context.get('abstract', '')[:100]}...") print("---") ``` **Search with Target URI Limitation** ```python # Search only in resources results = client.find( query="authentication", target_uri="viking://resources", ) # Search only in user memories results = client.find( query="preferences", target_uri="viking://~/memories" ) # Search only in current-user resources results = client.find( query="private docs", target_uri="viking://~/resources" ) # Search with the peer collection filtered to one peer peer_client = ov.SyncHTTPClient( url="http://localhost:1933", api_key="your-key", actor_peer_id="web-visitor-alice", ) peer_results = peer_client.find(query="invoice follow-up") # Search only in skills results = client.find( query="web search", target_uri="viking://~/skills" ) # Search in specific project results = client.find( query="API endpoints", target_uri="viking://resources/my-project", ) ``` **TypeScript SDK** ```typescript console.log(await client.find("authentication", { targetUri: "viking://resources/docs/" })); ``` **Go SDK** ```go result, err := client.Find(ctx, "how to authenticate users", &openviking.FindOptions{ TargetURI: "viking://resources/docs", Limit: 10, ContextType: []string{"resource"}, }) if err != nil { return err } for _, item := range result.Resources { fmt.Println(item.URI, item.Score) } ``` **CLI** ```bash # Basic search ov find "how to authenticate users" # Specify URI scope ov find "how to authenticate users" --uri "viking://resources" # Limit to context types ov find "authentication" --context-type memory,resource # With time filter ov find "invoice" --after 7d # With limit ov find "how to authenticate users" --limit 20 # Limit to specific level(s) (L0 only) ov find "how to authenticate users" --level 0 # Limit to specific level(s) (L1 and L2) using short option ov find "how to authenticate users" -L 1,2 # Image queries use only --image; pass a local path, viking://, http(s)://, or data:image URI ov find --image ./query.png --uri "viking://resources/images" --limit 5 # Search by an image already stored in VikingFS ov find --image "viking://resources/images/cat.png" --uri "viking://resources/images" --limit 5 # Search by a public image URL ov find --image "https://example.com/images/cat.png" --uri "viking://resources/images" --limit 5 # Combine text and image ov find "red poster style" --image ./poster.png --uri "viking://resources/images" ``` **Response Example** ```json { "status": "ok", "result": { "memories": [], "resources": [ { "context_type": "resource", "uri": "viking://resources/01-overview/API_Overview/Documentation_Reading_P_2c6ae38b.md", "level": 2, "score": 0.12808319406977778, "category": "", "match_reason": "", "abstract": "This document is an API documentation reading plan that outlines the structure of subsequent API reference materials organized by functional module. Main sections or topics covered include resource management API, search API, file system operations, ses...", "overview": null }, { "context_type": "resource", "uri": "viking://resources/01-overview/API_Overview/API_Endpoints/.abstract.md", "level": 0, "score": 0.12054087276495282, "category": "", "match_reason": "", "abstract": "This directory contains structured API reference documentation for the OpenViking platform, compiling detailed HTTP endpoint specifications for core and extended platform capabilities. It covers functional modules including system health checks, semanti...", "overview": null } ], "skills": [], "total": 2 } } ``` --- ### search() Semantic retrieval with optional session-aware query planning. #### 1. API Implementation Introduction `search()` can plan queries from session context. It calls the LLM only when `retrieval.enable_intent=true` and the session has a summary or messages. Without a session, with an empty session, or with intent analysis disabled, it uses the original query. Image queries skip session planning. **Processing Pipeline**: 1. Load session context (if session_id is provided) 2. Analyze query intent, understand actual needs combined with conversation history 3. Expand queries to improve recall rate 4. Run one global search per query; with a usable reranker, recall `2 × limit` candidates and rerank once to return at most `limit` results, otherwise recall `limit` hits directly 5. Return search results with query plan **Code Entry Points**: - `sdk/python/openviking_sdk/client.py:SyncHTTPClient.search()` - Python SDK entry (HTTP) - `openviking/retrieve/hierarchical_retriever.py:HierarchicalRetriever.retrieve()` - Core retrieval implementation - `openviking/server/routers/search.py:search()` - HTTP router - `crates/ov_cli/src/commands/search.rs:search()` - Rust CLI command #### 2. Interface and Parameter Description **Parameters** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | query | str | No | "" | Search query string. Required unless `image_url` is provided | | image_url | str or list[str] | No | None | One image URI or an ordered list of image URIs, each as a `data:image/...;base64,...`, `http(s)://`, or `viking://` URI. Requires a multimodal embedding model | | search_type | `"semantic"` \| `"keywords"` | No | `"semantic"` | Retrieval type. `semantic` uses embeddings and vector recall; `keywords` uses BM25 keyword recall | | mode | `"list"` \| `"context"` | No | `"list"` | Return ranked hits or assembled context. CLI `--mode` maps to this field; context-specific parameters are described below | | target_uri | str \| List[str] | No | "" | Limit search to specific URI prefix | | session | Session | No | None | Session for context-aware search (SDK) | | session_id | str | No | None | Session ID for context-aware search (HTTP) | | events_time_decay_protection | str \| null | No | null | Omit or pass `null` to disable decay. Pass `"0"` to decay immediately, or a duration such as `"7d"` to preserve the original score during that period and decay afterward. Supports non-negative integer `Xm`/`Xh`/`Xd` | | context_type | str \| List[str] | No | None | Limit results to one or more `ContextType` values: `memory`, `resource`, or `skill` | | tags | List[str] | No | None | Explicit retrieval tags in strict `k=v` form. Multiple tags are combined with AND; a result must contain every requested tag | | limit | int | No | 10 | Maximum results per planned query; list mode concatenates the results, so its total may exceed this value | | node_limit | int | No | None | Optional HTTP alias; overrides `limit` when provided | | score_threshold | float | No | None | Minimum relevance score threshold | | filter | Dict | No | None | Metadata filter | | since | str | No | None | Lower time bound, accepts `2h` or ISO 8601 / `YYYY-MM-DD`. Timezone-less values are interpreted as UTC. CLI `--after` maps to this field | | until | str | No | None | Upper time bound, accepts `30m` or ISO 8601 / `YYYY-MM-DD`. Timezone-less values are interpreted as UTC. CLI `--before` maps to this field | | time_field | "updated_at" \| "created_at" | No | "updated_at" | Metadata time field used by `since` / `until` | | level | int \| str \| List[int] | No | None | Limit results to specific level(s), e.g., `0`, `1`, `2`, or `0,1,2`. CLI `--level`/`-L` maps to this field | | include_provenance | bool | No | False | Include provenance/query-plan details in serialized result | | read_content | bool | No | False | Read each final matched URI with the visible-content read semantics and inline the result as `content`. Individual read failures leave the hit unchanged. Only supported by `mode="list"`. | | telemetry | bool \| object | No | False | Attach telemetry data to response | `search()` uses the same target resolution and explicit tag filtering rules as `find()`, including the peer collection filter selected by `X-OpenViking-Actor-Peer` or SDK `actor_peer_id`. When `image_url` is provided, `search()` uses direct image retrieval and skips session query planning. `keywords` requires a non-empty text query, does not support images, and requires a remote VikingDB backend with a full-text index. Event time decay applies to results tagged `memory_type=events` in semantic `find()` and both `search(mode="list")` and `search(mode="context")`. Memory extraction writes this tag on user/peer event L2 records; retrieval identifies events by the tag rather than inferring the type from a URI or level. Untagged/non-event results, query-less filter-only `find()`, `recall`, `grep`, and `glob` are unaffected. During event recall, the vector engine multiplies the original vector score by `time_score`. List responses expose this recall-stage vector score as `origin_score` and the factor as `time_score`; `score` is the final retrieval score, which may come from model rerank. Context mode uses that final score while assembling its candidates. The CLI labels list-result scores as semantic, time, and final scores. Inside the protection period `time_score` is 1, so the original score is unchanged. Time distance follows the VikingDB exponential decay operator, using the absolute difference from the request time. Time is read from the existing indexed `updated_at` field; no reindex or timestamp rewrite is required. Local fusion preserves the original score for missing or invalid timestamps; cloud fusion uses the indexed date-time field and the backend operator. The curve is owned by the server; callers only provide the per-request protection period. No `ov.conf` or `ovcli.conf` change is required. New and updated memories produced by memory extraction automatically receive a `memory_type=` search tag. With decay enabled, both local and cloud backends recall records tagged `memory_type=events` separately and merge them with the complementary untagged/non-event branch; both branches preserve the original scope, permission and level filters, without requiring a peer ID. Existing data is not backfilled; untagged memories keep their original scores. Direct content refreshes and ordinary tag updates preserve the existing memory type without inferring it from the URI. The local vector engine expands the requested event window by at most 3x internally (capped at 100,000), computes decay and sorts in C++, then returns top-k before any abstract or payload fields are fetched. This is a bounded candidate approximation, so events outside that semantic window are not guaranteed to be promoted. The HTTP vector service forwards the same native rule. Cloud adapters use VikingDB score fusion and request only the required limit plus offset, without a fixed 100,000-input override. cuVS collections use their native scalar/vector index for decay requests. openGauss accepts the forwarded parameters but does not apply time decay; it retains ordinary vector scoring. When model rerank is enabled, each recall branch requests `2 × limit` results. Event decay is already applied inside VikingDB for cloud backends, or inside the local C++ engine, before the two branches merge. The best `2 × limit` merged candidates go through one model rerank; model scores determine the final ranking and score threshold. There is no additional decay after the model. If rerank fails, retrieval keeps the already-decayed recall scores. `origin_score` and `time_score` remain recall-stage details, so their product need not equal a successful model rerank score. Parent-directory scores and hotness are not mixed into the result. The local engine's 3x expansion applies to the requested event window independently of the model's 2x window. For a final `limit=10`, each branch returns at most 20 results, the local event engine considers at most 60 vector candidates internally, and at most 20 merged results reach the model. #### 3. Usage Examples **HTTP API** ``` POST /api/v1/search/search ``` ```bash curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "best practices", "session_id": "abc123", "context_type": "memory", "since": "2h", "time_field": "updated_at", "events_time_decay_protection": "1d", "limit": 10 }' ``` **Search without Session (Uses the Original Query)** ```bash curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "how to implement OAuth 2.0 authorization code flow" }' ``` **Keyword Search** ```bash curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "hnsw", "search_type": "keywords", "target_uri": "viking://resources/docs", "limit": 10 }' ``` **Image Search** ```bash curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "query": "similar poster", "image_url": "data:image/png;base64,...", "limit": 10 }' ``` **Python SDK** ```python import openviking_sdk as ov from openviking_sdk import TextPart client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Create session with conversation context session_info = client.create_session() session = client.session(session_id=session_info["session_id"]) session.add_message( role="user", parts=[TextPart(text="I'm building a login page with OAuth")], ) session.add_message( role="assistant", parts=[TextPart(text="I can help you with OAuth implementation.")], ) # Search understands conversation context results = client.search( query="best practices", session_id=session.session_id, options={ "context_type": "resource", "since": "2h", }, ) for context in results.get("resources", []): print(f"Found: {context['uri']}") print(f"Abstract: {context.get('abstract', '')[:200]}...") ``` **Search without Session** ```python # search can also be used without session # Without session content, it uses the original query without intent analysis results = client.search( query="how to implement OAuth 2.0 authorization code flow" ) for context in results.get("resources", []): print(f"Found: {context['uri']} (score: {context.get('score', 0.0):.3f})") ``` **Image Search** ```python results = client.search( query="similar poster", image="/path/to/poster.png", ) ``` **TypeScript SDK** ```typescript console.log(await client.search("authentication", { targetUri: "viking://resources/docs/" })); ``` **Go SDK** ```go result, err := client.Search(ctx, "best practices", &openviking.SearchOptions{ SessionID: "abc123", ContextType: "resource", Limit: 10, }) if err != nil { return err } fmt.Println(result.Total) ``` **CLI** ```bash # Search with session ID ov search "best practices" --session-id abc123 # Assemble context using the server defaults, with event time decay ov search "recent decisions" --mode context --events-time-decay-protection 7d # Return the complete context result as JSON ov -o json search "best practices" --mode context # Limit to a context type ov search "best practices" --context-type skill # Search with time filter ov search "watch vs scheduled" --after 2026-03-15 --before 2026-03-20 # Rank user and peer event memories with time decay ov search "recent decisions" --context-type memory --level 2 \ --events-time-decay-protection 1d # Search without session (uses the original query) ov search "how to implement OAuth 2.0 authorization code flow" # BM25 keyword retrieval ov search "SearchByKeywords" --search-type keywords --uri "viking://resources/docs" # Limit to specific level(s) (L0 only) ov search "best practices" --level 0 # Limit to specific level(s) (L1 and L2) using short option ov search "how to implement OAuth" -L 1,2 # Image queries also use --image; they use direct retrieval and skip session planning ov search "similar poster" --image ./poster.png --uri "viking://resources/images" ``` `ov search --mode list` is the default and returns the usual hit list. With `--mode context`, the default terminal output prints the assembled `rendered` text (or a non-empty `digest` when returned); `-o json` preserves the full result, including `entries`, `rendered`, `digest`, and `stats`. The CLI uses server defaults for context-specific options such as `max_tokens`, `detail`, and `rewrite`; it does not expose flags for these options yet. Context mode rejects `--uri` and `--read-content`; the server ignores `--level` because assembly selects content tiers. **Response Example** ```json { "status": "ok", "result": { "memories": [], "resources": [ { "context_type": "resource", "uri": "viking://resources/docs/oauth-best-practices/.overview.md", "level": 1, "score": 0.95, "category": "", "match_reason": "Context-aware match: OAuth login best practices", "abstract": "OAuth 2.0 best practices for login pages...", "overview": "This guide covers OAuth 2.0 best practices including secure token handling, redirect URI validation, and state parameter usage..." } ], "skills": [], "query_plan": { "reasoning": "User is asking about OAuth implementation best practices, expanding to related security topics", "queries": [ { "query": "OAuth 2.0 best practices", "context_type": "resource", "intent": "Find OAuth 2.0 implementation guidelines", "priority": 3 }, { "query": "login page security", "context_type": "resource", "intent": "Find login page security recommendations", "priority": 2 } ] }, "total": 1 } } ``` --- ### search(mode="context") Assemble retrieval results into an injection-ready context block. `mode="list"` (the default) returns the ranked hit list and behaves exactly like the previous `search()`; `mode="context"` applies the token budget, selects detail levels, deduplicates across turns, and optionally produces an LLM digest in one server request. #### 1. Implementation Overview Injecting context every turn used to mean searching per type, reading each hit back, and stitching the block together client-side. With assembly on the server, a plugin sends one request and every harness shares one budgeting, degradation and dedup implementation. **Pipeline**: 1. **L1 query understanding**: optional bounded intent expansion from the session's recent messages (at most 3 queries, timeout fuse, falls back to the original query) 2. **L0 retrieval**: bucketed per `quotas`, or a single whole-scope search when quotas are off 3. **L2 assembly**: tier filling inside the token budget (everyone at their category's default tier first, then leftover budget deepens in score order); an oversized tier falls back instead of being truncated 4. **L3 rewrite**: optional digest with URI citations (timeout fuse; on failure the unrewritten `rendered` is still returned; an exact `NO_RELEVANT_MEMORY` result is reported as `stats.rewrite="no_relevant"` so Coding Agent clients inject nothing instead of falling back to `rendered`) **Code entry points**: - `openviking/server/routers/search.py:_search_context()` - HTTP route branch - `openviking/retrieve/context_assembler/pipeline.py:assemble_context()` - assembly orchestration - `openviking/retrieve/context_assembler/budget.py:plan_entries()` - budgeting and tier filling - `openviking/retrieve/context_assembler/tiers.py` - overview extraction per source type #### 2. Parameters **L0 retrieval domain**: `query`, `image_url`, `context_type`, `limit`, `score_threshold`, `filter`, `tags`, `since`/`until`, and the optional `events_time_decay_protection` behave as in list mode. `limit` applies only to quota-free retrieval. Once `purpose` or explicit `quotas` enables bucketed retrieval, the sum of the per-category quotas is the candidate ceiling. `target_uri` is not supported in context mode yet (returns 400); `level` is ignored because `detail` governs tiers. **L1 query understanding** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `session_id` | str | None | Required to enable query expansion and server-side dedup | | `query_expansion` | `off` \| `auto` | `auto` | Bounded session-aware expansion; falls back to the original query without a session or on failure | **L2 assembly** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | int | 10 | Candidate ceiling for quota-free retrieval only; ignored when `purpose` or `quotas` enables bucketed retrieval | | `max_tokens` | int | 1600 | The single budget parameter, estimated with a CJK-aware heuristic (codepoint ≥ 0x3000 counts 1.5 tok/char, otherwise chars/4) | | `quotas` | object | None | Per-bucket first-pass limits whose sum caps the candidates; slots a bucket leaves unused go to the best remaining hits of the other buckets, and `0` turns a bucket off. Keys are `events`/`entities`/`preferences`/`experiences`/`resources`/`skills`. Explicit quotas ignore `limit` | | `purpose` | `chat` \| `coding` | None | Enables six-domain bucket sampling with the preset quotas below. Applies only when `quotas` is not given | | `detail` | `abstract` \| `overview` \| `full` \| object | None | Requests one starting/maximum tier for every entry. Entries whose requested tier is unavailable or does not fit step down instead of being truncated. Omitted, each category takes its default tier (below). Also accepts a per-category object such as `{"events":"overview","preferences":"abstract"}`; categories left out keep their default. `"auto"` is a deprecated spelling and behaves as if omitted | | `dedup_turns` | int | 0 | Cooldown window in turns; needs `session_id`. Ledger lives at `{session_uri}/.recall_log.json` | | `exclude_uris` | string[] | [] | Stateless dedup fallback, up to 200 entries, unioned with `dedup_turns`. A directory URI excludes its whole subtree, so echo a served directory as its `.abstract.md` and `.overview.md` URIs to exclude just that node; `dedup_turns` cools served URIs exactly | | `peer_scope` | `actor` \| `all` | `all` | `actor` excludes other peers while keeping global, self-owned and current-actor content | | `other_peer_penalty` | number \| object | per-category defaults | Score penalty applied to other-peer hits | **L3 rewrite** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `rewrite` | bool \| `auto` | `false` | Server-side digest rewrite; `auto` engages only when a query_planner model is configured | | `rewrite_max_bullets` | int | 6 | Digest bullet ceiling (1–20) | **Tier rules** - **Purpose presets**: `chat` uses `events:3, entities:3, preferences:1, experiences:1, resources:1, skills:1`; `coding` uses `events:1, entities:2, preferences:1, experiences:1, resources:3, skills:2`. Each bucket first takes up to its own quota; when a bucket finds fewer matches, its unused slots go to the best remaining hits of the other buckets, so the total (10 for either preset) is never exceeded. Results are deduplicated and globally sorted after gathering, but are not truncated by a second global `limit` - **Default tier per category**: with `detail` omitted, each category lands on the tier below. Among file entries, only `events` needs a body read at its default tier. Directory hits read `.overview.md`. Preset scope directories (`viking://resources`, `viking://agent` and its preset children, the user root and its first-level directories) serve only for navigation and are never returned as entries, even after their `.overview.md` has been regenerated from their contents | Category | Default tier | Leftover budget may reach | Why | |----------|--------------|---------------------------|-----| | `events` | overview | full | The one memory type whose body is long enough for `# Summary` extraction to be a real compression | | `entities` / `preferences` / `experiences` | abstract | abstract | Short bodies, and the writer stores the whole body in the abstract scalar, so abstract already is the complete file | | `resources` / `skills` | abstract | abstract | A resource shows the 256-char abstract from semantic processing, a skill the name/description generated from its `SKILL.md` frontmatter; bodies can be large or carry credentials, so deepening is opt-in | | `memories` | abstract | abstract | Built-in memory types outside the four named ones — `cases`, `patterns`, `tools`, `trajectories`, skill-usage memories. Only quota-free retrieval reaches them; they own no bucket, so `quotas` cannot name them, but `detail` and `other_peer_penalty` can | | Directory hits | overview | overview | Directory entries use the `.overview.md` sidecar and do not expand the entire subtree. Skill package hits are the exception: they normalize onto `/SKILL.md` and follow the file rules above | - **Skill packages**: a package stores one vector per file and per directory level, and they all collapse into a single entry before the quotas apply — one entry per package, one quota slot, whichever file inside it matched. That entry's `uri` is `/SKILL.md`, the same path `/skills/find` reports as `skill_md_uri`, and its text is the package's own abstract; a package whose abstract has not been generated yet degrades to a bare `uri` rather than borrowing the summary of the file that matched. `dedup_turns` therefore cools a whole package: once one is served at `abstract` or deeper, a hit on any file inside it is skipped for the rest of the window - **Floor**: every result carries at least its `uri`. When a memory abstract is unavailable or busts the per-entry cap, the entry falls back to overview: the memory writer stores the whole body in that scalar, so for memory categories overview sits *below* abstract on the content ladder and the substitute discloses less. A `resources` or `skills` abstract is the short generated summary instead, so the same substitution would read a body the caller never asked for — those two degrade to a bare `uri` rather than deepen - **Explicit `detail`**: sets that tier as both the requested start and ceiling; entries that do not fit still step down a tier rather than being truncated. The memory overview substitute above is the one case where the served `detail` can outrank the pin, and only because it carries less content than the pinned tier would - **Overview by source type**: memory files use the leading `# Summary` section, code files use class and function signatures (reusing `code_outline`), long documents use the heading tree plus first paragraph - **Per-entry cap**: `max_tokens ÷ candidate_count × 2`, applied to every tier except the bare `uri`; a tier exceeding it falls back to the previous tier rather than being truncated. If budget is still left over, one final deepening pass ignores the cap and is bounded only by `max_tokens` #### 3. Examples **HTTP API** ```bash # Basic context assembly curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENVIKING_API_KEY" \ -d '{"query":"what changed on this branch","mode":"context","max_tokens":1600}' # Session-aware: query expansion plus cross-turn dedup curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENVIKING_API_KEY" \ -d '{ "query":"continue that refactor", "mode":"context", "session_id":"cc-1a2b3c", "query_expansion":"auto", "dedup_turns":5, "purpose":"coding", "max_tokens":3000 }' # With the server-side digest rewrite curl -X POST http://localhost:1933/api/v1/search/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENVIKING_API_KEY" \ -d '{"query":"tier design","mode":"context","max_tokens":3000,"rewrite":true}' ``` **Response excerpt** `entries`, text, and `rendered` are shortened below; the statistics describe the full 13-entry response. ```json { "status": "ok", "result": { "entries": [ { "uri": "viking://user/default/memories/events/2026/07/14/tier_design.md", "category": "events", "score": 0.45, "detail": "full", "text": "# Summary\nTiers now take a per-category default\n...", "origin": "self" }, { "uri": "viking://user/default/memories/entities/software/openviking_fs.md", "category": "entities", "score": 0.43, "detail": "abstract", "text": "OpenViking FS storage layer...", "origin": "self" } ], "rendered": "\n# Summary\n...\n", "digest": "", "stats": { "candidates": 13, "returned": 13, "dropped": 0, "deduped": 0, "max_tokens": 3000, "used_tokens": 2510, "per_entry_cap": 462, "detail": null, "tier_counts": {"full": 4, "overview": 2, "abstract": 7}, "fill": {"floor_tokens": 1890, "overview_upgrades": 0, "full_upgrades": 4, "spare_upgrades": 0}, "query_expansion": "used", "rewrite": "off", "rewrite_usage": null, "excluded": 0, "dedup": {"turns": 5, "status": "ok", "cooled": 2, "turn": 34} } } } ``` | Field | Type | Description | |-------|------|-------------| | `entries[].uri` | string | Entry URI, always present at every tier, expandable with the MCP `read` tool | | `entries[].category` | string | `events`/`entities`/`preferences`/`experiences`/`resources`/`skills`, or `memories` for a built-in memory type outside those four | | `entries[].detail` | string | Tier actually served: `full`, `overview`, `abstract` or `uri` | | `entries[].text` | string | Body for that tier; empty at the `uri` tier | | `rendered` | string | Flat XML context block, ready to inject; empty when rewrite reports `no_relevant` | | `digest` | string | Digest when the rewrite succeeded, empty string on failure or when the compressor reports no relevant memory | | `stats` | object | Budget usage, tier distribution, expansion and rewrite status (`off`, `ok`, `no_relevant`, `failed` or `timeout`), dedup ledger state; carries `retrieval_errors` when a retrieval scope failed, so a broken index is distinguishable from having no relevant memories | When `stats.rewrite` is `no_relevant`, the response keeps `entries` for inspection but returns both `digest` and `rendered` as empty strings. This makes the successful empty result safe for clients that predate the explicit status. Nothing was served that turn, so those URIs also stay out of the `dedup_turns` ledger and remain available to the later turn they are relevant to. **Validation rules** - Any context-only parameter sent explicitly under `mode="list"` → 400 - `target_uri` under `mode="context"` → 400 - Unknown `quotas` key → 400 - Fields ignored in context mode (`level`, and `limit` when `purpose` or explicit quotas are active) are reported in `stats.ignored` --- ### grep() Search content by pattern (regex). #### 1. API Implementation Introduction The `grep()` method performs regex pattern matching search in the file system, used to find files and content lines containing specific patterns. Unlike semantic search, grep is exact pattern matching. **Processing Pipeline**: 1. Traverse file system starting from specified URI 2. Perform regex matching on each file content 3. Collect matching lines, position information, and optional surrounding context 4. Return matching results list **Code Entry Points**: - `sdk/python/openviking_sdk/client.py:SyncHTTPClient.grep()` - Python SDK entry (HTTP) - `openviking/server/routers/search.py:grep()` - HTTP router - `crates/ov_cli/src/commands/search.rs:grep()` - Rust CLI command #### 2. Interface and Parameter Description **Parameters** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | uri | str | Yes | - | Viking URI to search in | | pattern | str | Yes | - | Search pattern (regex) | | case_insensitive | bool | No | False | Ignore case | | exclude_uri | str | No | None | URI prefix to exclude from search | | node_limit | int | No | 256 | Maximum number of results. Omitted requests default to 256; pass a larger integer when you need more results | | level_limit | int | No | 10 | Maximum directory depth to traverse. HTTP SDKs use the server default when omitted. | | tags | string[] | No | Unset | Search only files matching every supplied `k=v` retrieval tag | | include_tags | bool | No | `false` | Include each matched file's retrieval tags without filtering | | before_context | int | No | 0 | Number of context lines returned before each match; supported by the HTTP API and CLI | | after_context | int | No | 0 | Number of context lines returned after each match; supported by the HTTP API and CLI | `tags` uses AND semantics and filters candidate files before content matching and `node_limit` truncation. For example, `["team=search", "env=prod"]` matches only files carrying both tags. Entries in `matches` include `tags` when `tags` filtering is used or `include_tags=true` is requested; files without retrieval tags then return an empty array (`[]`). Plain grep omits `tags` to avoid an unnecessary VectorDB read. #### 3. Usage Examples **HTTP API** ``` POST /api/v1/search/grep ``` ```bash curl -X POST http://localhost:1933/api/v1/search/grep \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "uri": "viking://resources", "pattern": "authentication", "case_insensitive": true, "before_context": 1, "after_context": 1, "tags": ["team=search", "env=prod"] }' ``` **Python SDK** ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() results = client.grep( uri="viking://resources", pattern="authentication", case_insensitive=True, node_limit=1024, tags=["team=search", "env=prod"], ) print(f"Found {results['count']} matches") for match in results['matches']: print(f" {match['uri']}:{match['line']}") print(f" {match['content']}") ``` **TypeScript SDK** ```typescript console.log(await client.grep("viking://resources/docs/", "authentication", { tags: ["team=search", "env=prod"], })); ``` **Go SDK** ```go nodeLimit := 1024 result, err := client.Grep(ctx, "viking://resources", "authentication", &openviking.GrepOptions{ CaseInsensitive: true, NodeLimit: &nodeLimit, Tags: []string{"team=search", "env=prod"}, }) if err != nil { return err } fmt.Println(result["count"]) ``` **CLI** ```bash # Basic search ov grep "authentication" --uri viking://resources # Ignore case ov grep "authentication" --uri viking://resources --ignore-case # Specify depth limit ov grep "TODO" --uri viking://resources --level-limit 3 # Return two context lines before and after each match ov grep "authentication" --uri viking://resources -b 2 -a 2 # Search only files carrying every tag ov grep "TODO" --uri viking://resources --tags team=search,env=prod # Include tags in human-readable results without filtering ov grep "TODO" --uri viking://resources --fields tags ``` For HTTP `POST /api/v1/search/grep`, set `include_tags: true` to include tags without filtering. A request with `tags` always returns tags for the matched files. **Response Example** ```json { "status": "ok", "result": { "matches": [ { "uri": "viking://resources/docs/auth.md", "line": 15, "content": "User authentication is handled by...", "before_context": [ { "line": 14, "content": "## Authentication" } ], "after_context": [ { "line": 16, "content": "Configure an API key before sending requests." } ], "tags": [ "team=search", "env=prod" ] } ], "count": 1 } } ``` --- ### glob() Match files by glob pattern. #### 1. API Implementation Introduction The `glob()` method uses file wildcard pattern matching URIs, similar to Unix shell glob functionality. Used to find files and directories by name patterns. **Supported Pattern Syntax**: - `*` matches any character (except path separator) - `**` recursively matches any directory - `?` matches single character - `[]` matches character range **Code Entry Points**: - `sdk/python/openviking_sdk/client.py:SyncHTTPClient.glob()` - Python SDK entry (HTTP) - `openviking/server/routers/search.py:glob()` - HTTP router - `crates/ov_cli/src/commands/search.rs:glob()` - Rust CLI command #### 2. Interface and Parameter Description **Parameters** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | pattern | str | Yes | - | Glob pattern (e.g., `**/*.md`) | | uri | str | No | "viking://" | Starting URI | | node_limit | int | No | 256 | Maximum number of matches to return. Omitted requests default to 256; pass a larger integer when you need more results | | extra_fields | list[str] | No | None | Extra fields to include per match. Recognized names: `name`, `uri`, `path`, `type`, `size`, `mode`, `mtime`, `locked`, `id`. When omitted, the response contains URI strings only; when provided, `result.matches` becomes a list of entry objects | | tags | string[] | No | Unset | Retain only matches that have every supplied `k=v` retrieval tag | | include_tags | bool | No | `false` | Return retrieval tags for each match without filtering | `tags` uses AND semantics and is applied before `node_limit`. A tagged or `include_tags=true` request returns entry objects with a `tags` array; ordinary glob requests retain URI-string results and do not read tags from VectorDB. #### 3. Usage Examples **HTTP API** ``` POST /api/v1/search/glob ``` ```bash curl -X POST http://localhost:1933/api/v1/search/glob \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{ "pattern": "**/*.md", "uri": "viking://resources", "tags": ["team=search", "env=prod"], "include_tags": true }' ``` **Python SDK** ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Find all markdown files (defaults to returning at most 256 matches) results = client.glob(pattern="**/*.md", uri="viking://resources") print(f"Found {results['count']} markdown files:") for uri in results['matches']: print(f" {uri}") # Find Markdown files matching all supplied tags results = client.glob( pattern="**/*.md", uri="viking://resources", tags=["team=search", "env=prod"], ) print(f"Found {results['count']} tagged markdown files") for entry in results['matches']: print(f" {entry['uri']} {entry['tags']}") ``` **TypeScript SDK** ```typescript console.log(await client.glob("**/*.md", "viking://resources/docs/", { tags: ["team=search", "env=prod"], })); ``` **Go SDK** ```go result, err := client.Glob(ctx, "**/*.md", "viking://resources", &openviking.GlobOptions{ NodeLimit: openviking.Int(1024), Tags: []string{"team=search", "env=prod"}, }) if err != nil { return err } fmt.Println(result["count"]) ``` **CLI** ```bash # Find all markdown files ov glob "**/*.md" --uri viking://resources # Find all Python files ov glob "**/*.py" # Filter by all tags, or project tags without filtering ov glob "**/*.md" --tags team=search,env=prod ov glob "**/*.md" -f tags # Table output with extra fields (ps -o style -f) ov glob "**/*.py" -f name,size,mtime,mode # Script-friendly simple output with selected fields (comma-separated, no header) ov glob "**/*.py" --simple -f name,size ``` **Response Example** Default (URI strings): ```json { "status": "ok", "result": { "matches": [ "viking://resources/docs/api.md", "viking://resources/docs/guide.md" ], "count": 2 } } ``` With `extra_fields=["name","size","mtime"]`: ```json { "status": "ok", "result": { "matches": [ { "name": "api.md", "uri": "viking://resources/docs/api.md", "size": 12345, "mtime": 1720000000 }, { "name": "guide.md", "uri": "viking://resources/docs/guide.md", "size": 8234, "mtime": 1720000001 } ], "count": 2 } } ``` --- ## Working with Results ### Read Content Progressively Results include an abstract and the matched URI. Semantic L0/L1 hits point to `.abstract.md` or `.overview.md`; use their parent directory with `overview()`. L2 hits point to files that `read()` can return directly. **Python SDK** ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() results = client.find(query="authentication") for context in results.get("resources", []): # Start with L0 (abstract) - already in context["abstract"] print(f"Abstract: {context.get('abstract', '')}") if context.get("level", 0) < 2: # Get L1 (overview) for directories directory_uri = context["uri"].rsplit("/", 1)[0] overview = client.overview(uri=directory_uri) print(f"Overview: {overview[:500]}...") else: # Load L2 (content) for files content = client.read(uri=context["uri"]) print(f"File content: {content}") ``` **HTTP API** ```bash # Step 1: Search curl -X POST http://localhost:1933/api/v1/search/find \ -H "Content-Type: application/json" \ -H "X-API-Key: your-key" \ -d '{"query": "authentication"}' # Step 2: Read overview for directory result curl -X GET "http://localhost:1933/api/v1/content/overview?uri=viking://resources/docs/auth" \ -H "X-API-Key: your-key" # Step 3: Read full content for file result curl -X GET "http://localhost:1933/api/v1/content/read?uri=viking://resources/docs/auth.md" \ -H "X-API-Key: your-key" ``` ## Best Practices ### Use Specific Queries ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Good - specific query results = client.find(query="OAuth 2.0 authorization code flow implementation") # Less effective - too broad results = client.find(query="auth") ``` ### Scope Your Searches ```python import openviking_sdk as ov client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Search in relevant scope for better results results = client.find( query="error handling", target_uri="viking://resources/my-project", ) ``` ### Use Session Context for Conversations ```python import openviking_sdk as ov from openviking_sdk import TextPart client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # For conversational search, use session session_info = client.create_session() session = client.session(session_id=session_info["session_id"]) session.add_message( role="user", parts=[TextPart(text="I'm building a login page")], ) # Search understands context results = client.search( query="best practices", session_id=session.session_id, ) ``` ## Related Documentation - [Resources](02-resources.md) - Resource management - [Sessions](05-sessions.md) - Session context - [Context Layers](../concepts/03-context-layers.md) - L0/L1/L2