syntax = "proto3"; package hermes; // Search service service SearchService { // Search for documents rpc Search(SearchRequest) returns (SearchResponse); // Get document by ID rpc GetDocument(GetDocumentRequest) returns (GetDocumentResponse); // Get index info rpc GetIndexInfo(GetIndexInfoRequest) returns (GetIndexInfoResponse); // Text statistics (document frequencies, corpus sizes, average lengths) // of the BM25 terms of a query over this backend's index. A broker that // scatters one index over several shards sums the responses and sends the // total back as `SearchRequest.text_stats`, so every shard scores with // the same IDF. rpc GetTextStats(GetTextStatsRequest) returns (GetTextStatsResponse); } // Index service service IndexService { // Create a new index (supports both structured schema and SDL string) rpc CreateIndex(CreateIndexRequest) returns (CreateIndexResponse); // Add documents to index (streaming) rpc IndexDocuments(stream IndexDocumentRequest) returns (IndexDocumentsResponse); // Add documents in batch rpc BatchIndexDocuments(BatchIndexDocumentsRequest) returns (BatchIndexDocumentsResponse); // Stage whole-document deletions by exact primary key, including every chunk. rpc DeleteDocuments(DeleteDocumentsRequest) returns (DocumentMutationResponse); // Stage complete replacements (inserts if absent). Commit publishes them. rpc UpsertDocuments(UpsertDocumentsRequest) returns (DocumentMutationResponse); // Commit pending changes rpc Commit(CommitRequest) returns (CommitResponse); // Force merge segments rpc ForceMerge(ForceMergeRequest) returns (ForceMergeResponse); // Run bounded text, BMP, Seismic, and binary ANN layout maintenance rpc Reorder(ReorderRequest) returns (ReorderResponse); // Delete an index rpc DeleteIndex(DeleteIndexRequest) returns (DeleteIndexResponse); // List all indexes rpc ListIndexes(ListIndexesRequest) returns (ListIndexesResponse); // Retrain global vector codebooks and atomically rebuild every ANN segment rpc RetrainVectorIndex(RetrainVectorIndexRequest) returns (RetrainVectorIndexResponse); // Atomically replace one vector field's IVF/ScaNN algorithm or parameters rpc AlterVectorIndex(AlterVectorIndexRequest) returns (AlterVectorIndexResponse); } // Query types message Query { oneof query { TermQuery term = 1; BooleanQuery boolean = 2; BoostQuery boost = 3; AllQuery all = 4; SparseVectorQuery sparse_vector = 5; DenseVectorQuery dense_vector = 6; MatchQuery match = 7; RangeQuery range = 8; PrefixQuery prefix = 9; BinaryDenseVectorQuery binary_dense_vector = 10; FusionQuery fusion = 11; // Top-level only (cannot be nested) PhraseQuery phrase = 12; } } // Method for fusing sub-query result lists enum FusionMethod { FUSION_RRF = 0; // Reciprocal Rank Fusion (rank-based, default) FUSION_NORMALIZED_WEIGHTED_SUM = 1; // Min-max normalized weighted score sum // Return complete bounded nomination lists/union for coordinator-side fusion. FUSION_CANDIDATES = 2; } // Weighted sub-query for hybrid fusion message WeightedQuery { Query query = 1; float weight = 2; // Legacy RRF/NWS contribution; 0 or unset = 1.0 // Required, unique feature identity when l1 or score_export is supplied. string name = 3; ScoreScope scope = 4; // Backfill this branch without using it to nominate candidates (L1/export only). bool score_only = 5; } // Union fusion of independently-executed sub-queries (e.g. sparse + dense // hybrid retrieval). Unlike the reranker (which can only re-score documents // the first-stage query found), fusion keeps documents found by ANY // sub-query. Only valid at the top level of SearchRequest.query; composes // with SearchRequest.reranker (the fused list becomes the L1 candidates). message FusionQuery { repeated WeightedQuery queries = 1; FusionMethod method = 2; float rrf_k = 3; // RRF rank constant; 0 = default 60 // Combiner for fused per-chunk (ordinal) scores into a document score. // Fusion runs at chunk granularity: same-chunk hits across sub-queries // compound, and results carry per-chunk ordinal_scores. // Unset (0) = MAX (recommended; LogSumExp is unsuitable at RRF magnitudes). MultiValueCombiner combiner = 5; // Common document eligibility for every branch. Does not create an L1 feature. repeated Query filters = 6; // Independent per-branch nomination depth (L1/export only). 0 uses // candidate_limit. The complete union survives until L1 ranking/export. uint32 candidate_depth = 7; } // Scope is explicit: document features never become body ordinal zero. enum ScoreScope { SCORE_SCOPE_UNSPECIFIED = 0; SCORE_SCOPE_DOCUMENT = 1; SCORE_SCOPE_CHUNK = 2; } // Fixed preprocessing trained offline, never derived from a shard/result page. // The only L1 scoring contract: a compiled symbolic formula over branch scores. message L1Ranking { reserved 1, 2, 3, 6; reserved "weights", "bias", "transforms", "rrf_weight"; // Omitted=true: backfill only missing raw cells. False uses organic values. optional bool backfill = 4; // Raw defaults for missing formula variables; omitted variables use zero. map missing_values = 5; // Required, nonempty expression over named raw scores and reserved `rrf`. // Runs before passage/document selection. Requires formula_v1. string formula = 7; } message ScoreExport { // Maximum raw passage rows per hit; 0 = all (65536). With l1, exported rows // are the best passages after complete scoring. Without l1, exceeding this // bound fails instead of silently omitting features the caller needs to rank. uint32 passages_per_document = 1; // Diagnostics: score every stored passage of nominated documents. Default // false scores the union of nominated passages only, plus document context. bool all_passages = 2; // Capability 4: score real body ordinals for document-only nominees. // Requires backfill and a chunk-scoped feature. Existing body nominations // retain their original passage union; no organic votes are fabricated. bool seed_document_passages = 3; } // How to combine scores for multi-valued documents enum MultiValueCombiner { COMBINER_LOG_SUM_EXP = 0; // Softmax-weighted smooth maximum (default); count-invariant COMBINER_MAX = 1; // Take maximum score COMBINER_AVG = 2; // Average all scores COMBINER_SUM = 3; // Sum all scores COMBINER_WEIGHTED_TOP_K = 4; // Weighted top-k with decay } // Sparse vector query for similarity search // Either provide (indices, values) directly, or provide text for server-side tokenization message SparseVectorQuery { string field = 1; repeated uint32 indices = 2; // Pre-computed token indices repeated float values = 3; // Pre-computed token values string text = 4; // Raw text (tokenized server-side if tokenizer configured) MultiValueCombiner combiner = 5; // How to combine scores for multi-value fields float heap_factor = 6; // BMP/MaxScore pruning factor (0 = schema default, 1 = exact). float combiner_temperature = 7; // Temperature for LogSumExp (default: 1.5) uint32 combiner_top_k = 8; // K for WeightedTopK (default: 5) float combiner_decay = 9; // Decay for WeightedTopK (default: 0.7) float weight_threshold = 10; // Min abs(weight) for query dims (0 = no filtering) uint32 max_query_dims = 11; // Max query dimensions to process (0 = all) float pruning = 12; // Fraction of query dims to keep (0 = no pruning, 0.1 = top 10%) optional uint32 lsp_gamma = 13; // BMP LSP gamma (unset = depth-derived, 0 = exhaustive). optional uint32 seismic_cut = 14; // Nomination query dimensions (1..64) optional float seismic_factor = 15; // Summary pruning factor (0..1) optional bool exhaustive = 16; // Exact shared-forward scan; false allows approximation } // Dense vector query for similarity search message DenseVectorQuery { string field = 1; repeated float vector = 2; uint32 nprobe = 3; // Number of clusters to probe (for IVF indexes) MultiValueCombiner combiner = 5; // How to combine scores for multi-value fields float combiner_temperature = 6; // Temperature for LogSumExp (default: 1.5) uint32 combiner_top_k = 7; // K for WeightedTopK (default: 5) float combiner_decay = 8; // Decay for WeightedTopK (default: 0.7) } // Binary dense vector query for Hamming distance search message BinaryDenseVectorQuery { string field = 1; bytes vector = 2; // Packed-bit query vector (ceil(dim/8) bytes) MultiValueCombiner combiner = 3; // How to combine scores for multi-value fields float combiner_temperature = 4; uint32 combiner_top_k = 5; float combiner_decay = 6; } message TermQuery { string field = 1; string term = 2; // Optional hint for the field's tokenizer (see MatchQuery.tokenizer_hint) string tokenizer_hint = 3; } message BooleanQuery { repeated Query must = 1; repeated Query should = 2; repeated Query must_not = 3; } message BoostQuery { Query query = 1; float boost = 2; } message AllQuery {} // Range query on a fast field (inclusive bounds) // Exactly one pair of bounds should be set (u64, i64, or f64) message RangeQuery { string field = 1; optional uint64 min_u64 = 2; optional uint64 max_u64 = 3; optional int64 min_i64 = 4; optional int64 max_i64 = 5; optional double min_f64 = 6; optional double max_f64 = 7; } // Full-text match query - tokenizes text server-side and searches as OR of tokens // Use this instead of TermQuery when searching with natural language text message MatchQuery { string field = 1; string text = 2; // Optional hint passed to the field's tokenizer. Static tokenizers ignore // it; a `lex(by: languages, ...)` tokenizer // reads it as a comma-separated list of language codes ("ru,en") and stems // each query token with the first listed language of the token's script. // Empty = the tokenizer's default. string tokenizer_hint = 3; // Proximity rescoring (sequential dependence): the top candidates of the // BM25 pass gain a bonus for adjacent query terms occurring next to each // other (ordered windows) or within `proximity_window` positions // (unordered windows, half weight), scaled by this weight. 0 = off. // Approximate: the bonus applies to an over-fetched candidate pool. float proximity_weight = 4; // Unordered window size; 0 = 8. uint32 proximity_window = 5; // Approximate MaxScore: scale the pruning threshold by 1/heap_factor // (0/unset or 1 = exact, rank-safe; 0.8 prunes more aggressively at some // recall cost). Must be finite and in [0, 1]. This option applies to text queries. float heap_factor = 6; // Long queries: keep only the `max_terms` rarest (highest idf) tokens for // scoring; 0 = all tokens. Approximate. uint32 max_terms = 7; } // Phrase query - text is tokenized server-side with the field's tokenizer // (so stemming matches indexing) and the terms must occur at the distances // the tokenizer assigned (consecutively unless the tokenizer dropped stop // words between them, whose positions are kept as gaps) or within `slop` // positions of that. Requires the field to be indexed with token positions // (`indexed` or `indexed`); without positions the // engine degrades to a MUST of the terms. Scored with BM25 over the phrase // frequency (occurrences of the whole phrase) and the summed idf of its terms. // A phrase whose words are all stop words is rejected standalone and dropped // (with a server warning) when it is a clause of a BooleanQuery. message PhraseQuery { string field = 1; string text = 2; uint32 slop = 3; string tokenizer_hint = 4; // See MatchQuery.tokenizer_hint } // Prefix query - matches all documents containing any term starting with prefix // Score is always 1.0 (filter-style). Efficient as a MUST clause in BooleanQuery. message PrefixQuery { string field = 1; string prefix = 2; } // L2 reranker: rerank L1 candidates by exact vector distance // For dense vectors: set `vector` (f32). For binary dense vectors: set `binary_vector` (bytes). message Reranker { string field = 1; // Vector field (dense or binary dense) repeated float vector = 2; // Query vector (f32, for dense fields) MultiValueCombiner combiner = 4; float combiner_temperature = 5; uint32 combiner_top_k = 6; float combiner_decay = 7; uint32 matryoshka_dims = 8; // Matryoshka pre-filter dims (0 = disabled) bytes binary_vector = 9; // Query vector (packed bits, for binary dense fields) float rrf_k = 10; // Reciprocal Rank Fusion k (0 = disabled, typical: 60) } // Search request/response message SearchRequest { string index_name = 1; Query query = 2; uint32 limit = 3; uint32 offset = 4; repeated string fields_to_load = 5; Reranker reranker = 6; // Optional L2 reranker // First-stage candidate pool. 0 uses offset + limit; explicit values must // be between offset + limit and the 2x ceiling. uint32 candidate_limit = 7; // Anytime mode: wall-clock budget in milliseconds for the scoring phase. // Text (BM25) MaxScore executors stop at the deadline and return the best // results found so far; `SearchResponse.truncated` reports that it fired. // 0 = unbounded (exact top-k). uint64 time_budget_ms = 8; // Cross-shard text statistics for BM25 (see `GetTextStats`). When set they // replace the backend's own statistics for the listed fields and terms; // unset = the backend aggregates over its own segments. TextStats text_stats = 9; // Requires named, scoped fusion branches. The formula may reference the RRF feature. L1Ranking l1 = 10; // With no l1, exports the complete candidate union for external inference. // Export-only requests require offset=0 and a limit covering the whole union. ScoreExport score_export = 11; // Return RRF diagnostics alongside the requested ranking score. Requires // top-level fusion and complete nomination; does not change ranking. bool include_rrf_scores = 12; // Preserve bounded per-shard nomination and selection traces. Default false; // does not rerun subqueries or change retrieval/ranking budgets. bool tracing = 13; } message GetTextStatsRequest { string index_name = 1; // The query whose BM25 terms are collected (after this backend's own // tokenization); sparse, dense and filter clauses contribute nothing. Query query = 2; } message GetTextStatsResponse { TextStats stats = 1; } // BM25 statistics of a set of text terms. message TextStats { // Documents in the index (or the sum over shards). uint64 total_docs = 1; repeated TextFieldStats fields = 2; } message TextFieldStats { string field = 1; // Scoring units of the field: documents for a plain field, chunks for a // chunked one. uint64 corpus_size = 2; // Average length of a scoring unit in tokens. float avg_len = 3; repeated TermDocFreq terms = 4; } message TermDocFreq { // Index-time token bytes. bytes term = 1; uint64 doc_freq = 2; } // Unique document address: segment + local doc_id message DocAddress { string segment_id = 1; // Segment ID (hex, 32 chars) uint32 doc_id = 2; // Segment-local document ID } message SearchHit { DocAddress address = 1; float score = 2; map fields = 3; repeated OrdinalScore ordinal_scores = 4; // Per-ordinal scores for multi-value fields CandidateScores candidate_scores = 5; // Present only when score_export is requested. optional float rrf_score = 6; // Present when include_rrf_scores is requested. repeated RrfContribution rrf_contributions = 7; } // One organic nomination vote. Document context has no ordinal; chunk votes // are combined per ordinal before the document's fusion combiner is applied. message RrfContribution { uint32 query_index = 1; string query_name = 2; // Empty for unnamed legacy branches. uint32 rank = 3; // One-based rank in the complete merged nomination list. float score = 4; // Branch weight / (k + rank). optional uint32 ordinal = 5; } message CandidateScores { // Branch name -> raw score. Absent key means unavailable indexed data; // a valid nonmatch is explicitly 0. Negative dense scores are preserved. map document = 1; repeated PassageScores passages = 2; // Before limiting exported rows; L1 selected from all of these passages. uint32 scored_passages = 3; } message PassageScores { uint32 ordinal = 1; map scores = 2; // Final formula score when l1 is present, otherwise no ranking score. optional float l1_score = 3; } // List of field values — supports multi-value fields (e.g. multiple URIs per document) message FieldValueList { repeated FieldValue values = 1; } // Score contribution from a specific ordinal in a multi-valued field message OrdinalScore { uint32 ordinal = 1; // Which value in the multi-valued field (0-indexed) float score = 2; // Score contribution from this ordinal } message FieldValue { oneof value { string text = 1; uint64 u64 = 2; int64 i64 = 3; double f64 = 4; bytes bytes_value = 5; SparseVector sparse_vector = 6; DenseVector dense_vector = 7; string json_value = 8; // JSON serialized as string bytes binary_dense_vector = 9; // Packed-bit binary vector } } // Sparse vector with term indices and weights message SparseVector { repeated uint32 indices = 1; repeated float values = 2; } // Dense vector (float32 values) message DenseVector { repeated float values = 1; } message SearchResponse { repeated SearchHit hits = 1; uint64 total_hits = 2; uint64 took_ms = 3; SearchTimings timings = 4; // True when `time_budget_ms` expired before every candidate was scored; // the hits are then the best-so-far, not the exact top-k. bool truncated = 5; // Feature contracts: "formula_v1" or "feature_export_v2". string ranking_method = 6; // Present for FUSION_CANDIDATES. Hits hold hydrated union entries once; // these lists preserve independent branch scores and nominated ordinals. repeated FusionCandidateList fusion_candidates = 7; SearchTrace trace = 8; // Present only when tracing is requested. // Capability 4 acknowledgment, true only when every backend honored the // requested document-only passage seeding policy. Old adapters default false. bool seeded_document_passages = 9; } message SearchTrace { repeated ShardSearchTrace shards = 1; } message ShardSearchTrace { string shard_id = 1; // Populated by the broker; empty on direct server calls. string backend_id = 2; string index_name = 3; repeated QueryTrace queries = 4; repeated FusionCandidate selected = 5; // Shard results before broker selection. string ranking_method = 6; bool truncated = 7; repeated Query filters = 8; // Common fusion filters applied during nomination. } message QueryTrace { uint32 query_index = 1; string query_name = 2; Query query = 3; // Original branch/root expression, including nested clauses. ScoreScope scope = 4; bool score_only = 5; uint32 candidate_depth = 6; uint32 total_seen = 7; // All organic nominations within the requested depth, before fusion/L1/L2. repeated FusionCandidate candidates = 8; } message FusionCandidateList { uint32 query_index = 1; repeated FusionCandidate candidates = 2; } message FusionCandidate { DocAddress address = 1; float score = 2; repeated OrdinalScore ordinal_scores = 3; } // Detailed timing breakdown for search phases (all values in microseconds) message SearchTimings { uint64 search_us = 1; // L1 retrieval (query scoring across segments) uint64 rerank_us = 2; // L2 reranking (dense vector rescoring) uint64 load_us = 3; // Document field loading from store uint64 total_us = 4; // Wall-clock total (includes overhead) uint64 candidate_scoring_us = 5; // Missing-only feature backfill and formula selection. } // Get document request/response message GetDocumentRequest { string index_name = 1; DocAddress address = 2; } message GetDocumentResponse { map fields = 1; } // Index info request/response message GetIndexInfoRequest { string index_name = 1; } message GetIndexInfoResponse { string index_name = 1; uint32 num_docs = 2; uint32 num_segments = 3; // Schema in SDL format string schema = 4; // Memory usage breakdown (if available) MemoryStats memory_stats = 5; // Per-field vector statistics repeated VectorFieldStats vector_stats = 6; // Per text field: how it is tokenized, so clients pick query forms // without parsing the schema SDL. repeated TextFieldInfo text_fields = 7; uint32 candidate_scoring_version = 8; // 3 requires symbolic L1 formula; preserves organic scores, optional backfill and raw defaults. repeated string unprepared_candidate_fields = 9; // Fields requiring lookup preparation/migration. uint64 physical_num_docs = 10; // Includes tombstones; num_docs counts live rows. uint64 num_deleted_docs = 11; double deleted_ratio = 12; // num_deleted_docs / physical_num_docs; zero for an empty index. } // Tokenization facts of one text field. message TextFieldInfo { string field = 1; // The field uses a `lex(...)` tokenizer. bool lexical = 2; // A `lex(by: )` tokenizer: the query's `tokenizer_hint` selects // the stemmer (empty when the field has no hint field). string hint_field = 3; // `lex(variants: true)`: the written word is indexed next to its stem and // folded forms, so one clause per field suffices (phrases match the // written form, match queries the stem). bool variants = 4; // Token positions are indexed, so phrase queries verify adjacency. bool positions = 5; // `indexed`: every value is its own BM25 unit with an ordinal. bool chunked = 6; } // Per-field vector statistics (dense or sparse) message VectorFieldStats { string field_name = 1; string vector_type = 2; // "dense" or "sparse" uint64 total_vectors = 3; uint32 dimension = 4; // dim for dense, num_dimensions for sparse float avg_terms_per_vector = 5; // sparse only: avg postings per vector (0 for dense) } // Memory usage statistics message MemoryStats { // Total estimated memory usage in bytes uint64 total_bytes = 1; // Indexing buffer memory (pending documents not yet flushed) IndexingBufferStats indexing_buffer = 2; // Segment reader memory (loaded for search) SegmentReaderStats segment_reader = 3; } // Indexing buffer memory breakdown message IndexingBufferStats { uint64 total_bytes = 1; uint64 postings_bytes = 2; uint64 sparse_vectors_bytes = 3; uint64 dense_vectors_bytes = 4; uint64 interner_bytes = 5; uint64 position_index_bytes = 6; uint32 pending_docs = 7; uint32 unique_terms = 8; } // Segment-reader heap memory (search structures and decompressed caches). // File-backed mappings and pinned residency are intentionally excluded: // mapped file extent is not equivalent to resident memory. message SegmentReaderStats { uint64 total_bytes = 1; uint64 term_dict_cache_bytes = 2; uint64 store_cache_bytes = 3; uint64 sparse_index_bytes = 4; uint64 dense_index_bytes = 5; uint32 num_segments_loaded = 6; } // Create index request/response message CreateIndexRequest { string index_name = 1; // Schema definition string schema = 2; } message CreateIndexResponse { bool success = 1; } // Field entry for multi-value field support message FieldEntry { string name = 1; FieldValue value = 2; } // Named document for batch indexing // Uses repeated FieldEntry to support multi-value fields (same name, multiple values) message NamedDocument { repeated FieldEntry fields = 1; } // Batch index documents request message BatchIndexDocumentsRequest { string index_name = 1; repeated NamedDocument documents = 2; } message BatchIndexDocumentsResponse { uint32 indexed_count = 1; uint32 error_count = 2; repeated DocumentError errors = 3; } // Requests are bounded before conversion or writer admission. Both require a // primary-key schema and an explicit Commit. No cross-partition transaction. message DeleteDocumentsRequest { string index_name = 1; repeated string primary_keys = 2; // <=100,000 keys, <=8 MiB total key bytes } message UpsertDocumentsRequest { string index_name = 1; repeated NamedDocument documents = 2; // <=1,000 documents, <=32 MiB encoded request (<=200 MiB for one document) } message DocumentMutationResponse { // Admitted operations, not affected rows. A missing deletion is accepted. // Every input is accepted or has one error; Commit publishes accepted work. uint32 accepted_count = 1; repeated DocumentError errors = 2; } // Per-document error detail (e.g. duplicate primary key) message DocumentError { uint32 index = 1; // 0-based index in the request batch string error = 2; // Human-readable error message } // Index document request/response message IndexDocumentRequest { string index_name = 1; repeated FieldEntry fields = 2; } message IndexDocumentsResponse { uint32 indexed_count = 1; repeated DocumentError errors = 2; } // Commit request/response message CommitRequest { string index_name = 1; } message CommitResponse { bool success = 1; uint32 num_docs = 2; } // Force merge request/response message ForceMergeRequest { string index_name = 1; // Expensive physical removal of deleted rows, once per final output. // Omitted/false copies encoded data and carries deletion masks unchanged. bool compact = 2; } message ForceMergeResponse { bool success = 1; uint32 num_segments = 2; } // Delete index request/response message DeleteIndexRequest { string index_name = 1; } message DeleteIndexResponse { bool success = 1; } // List indexes request/response message ListIndexesRequest {} message ListIndexesResponse { repeated string index_names = 1; } // Retrain vector index request/response message RetrainVectorIndexRequest { string index_name = 1; } message RetrainVectorIndexResponse { bool success = 1; } message AlterVectorIndexRequest { string index_name = 1; string field_name = 2; // SDL field type/options, for example: // dense_vector<768, f16> [indexed] string field_schema = 3; } enum VectorIndexAlterState { VECTOR_INDEX_ALTER_STATE_UNSPECIFIED = 0; VECTOR_INDEX_ALTER_STATE_BUILT = 1; VECTOR_INDEX_ALTER_STATE_DEFERRED_FLAT = 2; VECTOR_INDEX_ALTER_STATE_PARAMETERS_ONLY = 3; } message AlterVectorIndexResponse { uint64 publication_generation = 1; VectorIndexAlterState state = 2; } // Reorder request/response message ReorderRequest { string index_name = 1; } message ReorderResponse { bool success = 1; uint32 num_segments = 2; }