BE-622: Move semantic search embedding generation into the graph#8920
Conversation
PR SummaryMedium Risk Overview A new
The TypeScript SDK drops Reviewed by Cursor Bugbot for commit f3b4fcf. Bugbot is set up for automated code reviews on this repo. Configure here. |
d7f20d7 to
dcb0ee2
Compare
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
There was a problem hiding this comment.
Pull request overview
Moves semantic-search embedding generation from the TypeScript/Temporal path into the Rust graph service so /entities/search and /entity-types/search can accept a semanticString and resolve it to an embedding server-side (OpenAI), gated by HASH_GRAPH_OPENAI_API_KEY.
Changes:
- Adds a new Rust workspace crate
hash-graph-embeddingswith a provider-agnosticEmbeddingGeneratortrait and an OpenAI-backed client implementation. - Updates graph REST search request handling to accept
embeddingxorsemanticString, resolve/validate embeddings, and classify provider failures into more specific status codes. - Removes the Node SDK’s Temporal-based
calculateEmbeddingpath and forwardssemanticStringdirectly; wires the API key into docker-compose and updates agent docs.
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds the new @rust/hash-graph-embeddings workspace package to Yarn dependencies. |
| libs/@local/graph/types/src/embedding.rs | Adds len() / is_empty() helpers used for embedding validation. |
| libs/@local/graph/sdk/typescript/src/entity.ts | Removes Temporal dependency and forwards semanticString directly to graph API search. |
| libs/@local/graph/sdk/typescript/src/entity-type.ts | Removes Temporal dependency and forwards semanticString directly to graph API search. |
| libs/@local/graph/sdk/typescript/src/embeddings.ts | Removes the Temporal-based calculateEmbedding implementation (keeps types). |
| libs/@local/graph/embeddings/src/openai.rs | Implements OpenAI embeddings client with retries, timeout, and response validation. |
| libs/@local/graph/embeddings/src/lib.rs | Introduces EmbeddingGenerator trait and re-exports public client types. |
| libs/@local/graph/embeddings/src/error.rs | Defines typed embedding-generation error taxonomy. |
| libs/@local/graph/embeddings/package.json | Adds Turborepo wiring + scripts for the new Rust crate package. |
| libs/@local/graph/embeddings/LICENSE.md | Adds crate-level license text. |
| libs/@local/graph/embeddings/docs/dependency-diagram.mmd | Adds dependency diagram for the new crate. |
| libs/@local/graph/embeddings/Cargo.toml | Adds the new crate to the Rust workspace with required deps. |
| libs/@local/graph/api/src/rest/mod.rs | Adds embedding resolution helper + embeds embedding client into REST dependencies. |
| libs/@local/graph/api/src/rest/entity.rs | Wires embedding client into entity search handler and resolves params asynchronously. |
| libs/@local/graph/api/src/rest/entity_type.rs | Wires embedding client into entity-type search handler and resolves params asynchronously. |
| libs/@local/graph/api/src/rest/entity_query_request.rs | Updates entity search request schema to embedding/semanticString XOR and resolves embedding server-side. |
| libs/@local/graph/api/package.json | Adds dependency on @rust/hash-graph-embeddings. |
| libs/@local/graph/api/openapi/openapi.json | Regenerates OpenAPI to reflect optional embedding and new semanticString. |
| libs/@local/graph/api/Cargo.toml | Adds Rust dependency on hash-graph-embeddings. |
| infra/compose/compose.yml | Wires HASH_GRAPH_OPENAI_API_KEY into the graph compose service. |
| Cargo.toml | Registers the new crate in the Rust workspace + adds reqwest-retry. |
| Cargo.lock | Locks new Rust dependencies (e.g. reqwest-retry, retry-policies). |
| apps/hash-graph/src/subcommand/server.rs | Adds HASH_GRAPH_OPENAI_API_KEY config and initializes embedding client at startup. |
| apps/hash-graph/package.json | Adds dependency on @rust/hash-graph-embeddings. |
| apps/hash-graph/Cargo.toml | Adds Rust dependency on hash-graph-embeddings. |
| AGENTS.md | Documents mise run sync:turborepo for Rust crate wiring changes. |
| .clippy.toml | Adds OpenAI to doc-valid-idents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
dcb0ee2 to
a8b9f33
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8920 +/- ##
==========================================
- Coverage 59.45% 59.42% -0.03%
==========================================
Files 1358 1357 -1
Lines 131792 131845 +53
Branches 6021 6018 -3
==========================================
- Hits 78353 78352 -1
- Misses 52509 52563 +54
Partials 930 930 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ration-into-graph
…c-search-embedding-generation-into-graph # Conflicts: # apps/hash-graph/docs/dependency-diagram.mmd # apps/hash-graph/src/subcommand/server.rs # libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd # libs/@local/codec/docs/dependency-diagram.mmd # libs/@local/codegen/docs/dependency-diagram.mmd # libs/@local/graph/api/docs/dependency-diagram.mmd # libs/@local/graph/api/src/rest/mod.rs # libs/@local/graph/authorization/docs/dependency-diagram.mmd # libs/@local/graph/store/docs/dependency-diagram.mmd # libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd # libs/@local/graph/types/docs/dependency-diagram.mmd # libs/@local/harpc/types/docs/dependency-diagram.mmd # libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd # libs/@local/temporal-client/docs/dependency-diagram.mmd # tests/graph/test-data/rust/docs/dependency-diagram.mmd
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
Purpose
Move semantic-search embedding generation out of the Node SDK (which did a Temporal roundtrip to the
aiworker) into the graph. The/entities/searchand/entity-types/searchendpoints now accept asemanticStringand resolve it to an embedding by calling OpenAI directly, configured viaHASH_GRAPH_OPENAI_API_KEY.Builds on the dedicated search endpoints from BE-471/BE-621 (#8910, merged); targets
main.Linear: BE-622
What does this change?
hash-graph-embeddingscrate — provider-agnosticEmbeddingGeneratortrait +OpenAiEmbeddingClient(own thinreqwest+reqwest-retryclient, noasync-openai). Modeltext-embedding-3-large/ 3072 dims, pinned to match the TS indexer.HASH_GRAPH_OPENAI_API_KEYwired in like the Temporal client; search endpoints acceptembeddingxorsemanticString; provider failures are classified (auth / rate-limit / outage) and mapped to 429/503/500 instead of a blanket 500; a caller-suppliedembeddingis validated againstEmbedding::DIM; startup logs whether semantic search is enabled.searchEntities/searchEntityTypesforwardsemanticStringdirectly and no longer take atemporalClient; deadcalculateEmbeddingremoved (CreateEmbeddings*types kept for the worker's indexing path).mise run sync:turborepotask documented inAGENTS.md;HASH_GRAPH_OPENAI_API_KEYwired into the compose graph service.Pre-merge checklist
publish = false; no other publishable-package changes.AGENTS.md); no Petrinaut changes.@rust/hash-graph-embeddings.How to test
Set
HASH_GRAPH_OPENAI_API_KEY, start the graph, andPOST /entities/searchwith{ "semanticString": "...", "maximumSemanticDistance": 0.7 }. Without a key configured, semantic-string search returns a clean "unavailable" error and the startup log warns; the precomputed-embeddingpath needs no key.Notes / follow-ups
Retry-After(intentionally fail-fast for the interactive path); splittingEmbeddingError::Requestbuild-vs-send.