BE-622: Move semantic search embedding generation into the graph#8920
BE-622: Move semantic search embedding generation into the graph#8920TimDiekmann wants to merge 2 commits into
Conversation
PR SummaryMedium Risk Overview Adds
The TypeScript graph SDK drops Reviewed by Cursor Bugbot for commit 3b5c957. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d7f20d7. 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.57% 59.56% -0.02%
==========================================
Files 1366 1365 -1
Lines 132760 132837 +77
Branches 6045 6042 -3
==========================================
+ Hits 79094 79121 +27
- Misses 52732 52782 +50
Partials 934 934 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:
|
Add a hash-graph-embeddings crate exposing a provider-agnostic EmbeddingGenerator trait and an OpenAI-backed client (reqwest + reqwest-retry). The graph now resolves a search request's semanticString to an embedding itself, configured via HASH_GRAPH_OPENAI_API_KEY and wired in like the Temporal client, removing the Temporal roundtrip from the Node SDK search path. The /entities/search and /entity-types/search endpoints accept embedding xor semanticString; the Node SDK searchEntities/searchEntityTypes forward semanticString directly and no longer take a temporalClient. Provider failures are classified (auth/rate-limit/outage) and mapped to appropriate HTTP statuses rather than a blanket 500, a caller-supplied embedding is validated against Embedding::DIM, and startup logs whether semantic search is enabled. Regenerate the OpenAPI spec, document the sync:turborepo task in AGENTS.md, and wire HASH_GRAPH_OPENAI_API_KEY into the compose graph service.
a8b9f33 to
62e76ab
Compare
86ef310 to
3b5c957
Compare
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.