perf: parallelize EmbedBatch using goroutines and errgroup#107
perf: parallelize EmbedBatch using goroutines and errgroup#107
Conversation
Refactor EmbedBatch in internal/integrations/ai/embedder.go to process embedding requests concurrently instead of serially. A semaphore channel caps in-flight goroutines at maxBatchConcurrency (10) to avoid overwhelming the API. Results are written to pre-indexed slice positions so no mutex is required. Also adds four unit tests covering empty input, order preservation, error propagation, and the concurrency cap. Closes #51 Co-authored-by: Kaviru Hapuarachchi <Kavirubc@users.noreply.github.com>
Simili Triage ReportNote Quality Score: 9.5/10 (Excellent) Classification
Quality Improvements
Similar Threads
Warning Possible Duplicate (Confidence: 98%) ⏳ This pull request will be automatically closed in 72 hours if no objections are raised. If you believe this is not a duplicate, please leave a comment explaining why. Generated by Simili Bot |
There was a problem hiding this comment.
Pull request overview
This PR refactors EmbedBatch to generate embeddings concurrently (bounded by a fixed concurrency limit) to speed up bulk indexing, and adds unit tests intended to validate the new behavior.
Changes:
- Parallelize
EmbedBatchusingerrgroup.WithContextplus a semaphore-style concurrency cap. - Add unit tests for empty input, order preservation, error propagation, and concurrency limiting.
- Add
golang.org/x/syncas a direct dependency.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| internal/integrations/ai/embedder.go | Implements concurrent EmbedBatch using errgroup and a semaphore-limited worker pattern. |
| internal/integrations/ai/embedder_test.go | Adds tests for EmbedBatch behavior (empty input, ordering, errors, concurrency). |
| go.mod | Adds golang.org/x/sync dependency for errgroup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func TestEmbedBatch_ConcurrencyLimit(t *testing.T) { | ||
| // Verify that EmbedBatch honours maxBatchConcurrency: concurrent in-flight | ||
| // requests should never exceed the limit. | ||
| var ( | ||
| inFlight atomic.Int32 | ||
| maxObserved atomic.Int32 | ||
| ) | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| cur := inFlight.Add(1) | ||
| defer inFlight.Add(-1) | ||
| // Update max observed — simple CAS loop. | ||
| for { | ||
| old := maxObserved.Load() | ||
| if cur <= old || maxObserved.CompareAndSwap(old, cur) { | ||
| break | ||
| } | ||
| } | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(200) | ||
| _, _ = w.Write(embeddingOKBody()) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| e := newTestEmbedder(srv.URL) | ||
| e.retryConfig = fastRetry | ||
|
|
||
| texts := make([]string, maxBatchConcurrency*3) | ||
| for i := range texts { | ||
| texts[i] = fmt.Sprintf("text-%d", i) | ||
| } | ||
|
|
||
| if _, err := e.EmbedBatch(context.Background(), texts); err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if got := maxObserved.Load(); got > int32(maxBatchConcurrency) { | ||
| t.Errorf("max concurrent requests = %d, want <= %d", got, maxBatchConcurrency) | ||
| } | ||
| } |
There was a problem hiding this comment.
TestEmbedBatch_ConcurrencyLimit may be ineffective/flaky because the handler responds immediately, so requests may not overlap enough to observe concurrency >1 (and the test only asserts maxObserved <= maxBatchConcurrency). Consider adding a small synchronized delay/barrier in the handler (e.g., block until N requests are in-flight or sleep for a short duration) so the test reliably exercises the concurrency limit and would fail if the limit were removed.
go.mod
Outdated
| golang.org/x/crypto v0.31.0 // indirect | ||
| golang.org/x/net v0.33.0 // indirect | ||
| golang.org/x/sync v0.18.0 // indirect | ||
| golang.org/x/sync v0.18.0 |
There was a problem hiding this comment.
golang.org/x/sync v0.18.0 is required twice (once in the main require block and again in the indirect require block). This duplication is atypical and will be rewritten by go mod tidy; it should appear only once (as a direct dependency).
| golang.org/x/sync v0.18.0 |
| embeddings := make([][]float32, len(texts)) | ||
| g, ctx := errgroup.WithContext(ctx) | ||
| sem := make(chan struct{}, maxBatchConcurrency) | ||
|
|
||
| for i, text := range texts { | ||
| embedding, err := e.Embed(ctx, text) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to embed text %d: %w", i, err) | ||
| } | ||
| embeddings[i] = embedding | ||
| i, text := i, text | ||
| g.Go(func() error { | ||
| sem <- struct{}{} | ||
| defer func() { <-sem }() | ||
|
|
||
| embedding, err := e.Embed(ctx, text) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to embed text %d: %w", i, err) | ||
| } | ||
| embeddings[i] = embedding | ||
| return nil | ||
| }) | ||
| } |
There was a problem hiding this comment.
The semaphore is acquired inside the goroutine, so EmbedBatch still spawns one goroutine per input text. For large batches this can create thousands of blocked goroutines and unnecessary memory/GC pressure; the concurrency cap only limits in-flight API calls, not goroutine count. Consider using errgroup.Group.SetLimit(maxBatchConcurrency) (or acquiring the semaphore before starting each goroutine / using a worker pool) so goroutine creation is also bounded.
| g.Go(func() error { | ||
| sem <- struct{}{} | ||
| defer func() { <-sem }() | ||
|
|
||
| embedding, err := e.Embed(ctx, text) |
There was a problem hiding this comment.
Semaphore acquisition (sem <- struct{}{}) does not respect context cancellation. If ctx is cancelled while this send is blocked (because the semaphore is full), the goroutine will remain stuck waiting for capacity, delaying cancellation and potentially causing EmbedBatch to hang until other calls return. Use a select on ctx.Done() when acquiring the semaphore (or rely on errgroup.SetLimit, which avoids this pattern).
| func TestEmbedBatch_ResultsPreserveOrder(t *testing.T) { | ||
| // Track the order in which requests arrive — each call returns a unique embedding | ||
| // so we can verify the output is indexed correctly regardless of goroutine scheduling. | ||
| var counter atomic.Int32 | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| n := counter.Add(1) | ||
| type embItem struct { | ||
| Embedding []float64 `json:"embedding"` | ||
| } | ||
| type embResp struct { | ||
| Data []embItem `json:"data"` | ||
| } | ||
| b, _ := json.Marshal(embResp{Data: []embItem{{Embedding: []float64{float64(n), 0.0, 0.0}}}}) | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(200) | ||
| _, _ = w.Write(b) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| e := newTestEmbedder(srv.URL) | ||
| e.retryConfig = fastRetry | ||
|
|
||
| texts := make([]string, 20) | ||
| for i := range texts { | ||
| texts[i] = fmt.Sprintf("text-%d", i) | ||
| } | ||
|
|
||
| results, err := e.EmbedBatch(context.Background(), texts) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if len(results) != len(texts) { | ||
| t.Fatalf("expected %d results, got %d", len(texts), len(results)) | ||
| } | ||
| for i, emb := range results { | ||
| if len(emb) == 0 { | ||
| t.Errorf("result[%d] is empty", i) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
TestEmbedBatch_ResultsPreserveOrder doesn't actually assert order preservation. The handler returns embeddings based on request arrival order (a shared counter), but the test only checks that each result is non-empty, so it would pass even if EmbedBatch returned embeddings in the wrong order. To verify ordering, make the response embedding depend deterministically on the request's input text (e.g., parse the JSON body and encode the input index into the embedding) and assert results[i] matches texts[i].
📝 WalkthroughWalkthroughThe PR parallelizes batch embedding by refactoring Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Caller
participant Embedder as EmbedBatch
participant Worker as Goroutine Pool
participant API as Embedding Service
Client->>Embedder: Call EmbedBatch([]texts)
Embedder->>Worker: Spawn up to 10 workers (errgroup)
loop for each text
Worker->>API: Send embed request (concurrent)
API-->>Worker: Return vector or error
Worker-->>Embedder: Place result at corresponding index
end
Note right of Embedder: Wait for errgroup to finish\naggregate errors, preserve order
Embedder-->>Client: Return ordered results or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@simili-bot Fix the comments added by copilot |
|
Claude finished @Kavirubc's task in 2m 42s —— View job Fixing Copilot Review Comments
Summary of changes (commit
|
- Replace manual semaphore channel with errgroup.SetLimit(maxBatchConcurrency) so goroutine creation is bounded and context cancellation is respected - Fix TestEmbedBatch_ResultsPreserveOrder to parse input text index from the request body and assert deterministic embedding values per position - Fix TestEmbedBatch_ConcurrencyLimit to add a 5ms handler delay so in-flight requests overlap and the concurrency cap is actually observable - Remove duplicate golang.org/x/sync entry from go.mod indirect block Co-authored-by: Kaviru Hapuarachchi <Kavirubc@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/integrations/ai/embedder.go (1)
184-194: Redundant loop variable capture in Go 1.22+.Since this project uses Go 1.24.0, the
i, text := i, textshadowing on line 185 is no longer necessary. Go 1.22+ creates fresh loop variables per iteration, eliminating the classic closure capture bug.♻️ Suggested simplification
for i, text := range texts { - i, text := i, text g.Go(func() error { embedding, err := e.Embed(ctx, text)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/integrations/ai/embedder.go` around lines 184 - 194, The loop in internal/integrations/ai/embedder.go unnecessarily re-shadows loop variables with `i, text := i, text`; since Go 1.22+ (we're on 1.24) the loop creates fresh variables per iteration, so remove the `i, text := i, text` line and let the goroutine closure use the existing `i` and `text`; keep the body calling `e.Embed(ctx, text)`, handling `err` and assigning into `embeddings[i]` as before (inside the `g.Go` function).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/integrations/ai/embedder_test.go`:
- Around line 122-123: The fmt.Sscanf call that parses req.Input into idx must
have its return values checked to avoid silent failures; change the call in
embedder_test.go to capture the scan count and error (e.g. n, err :=
fmt.Sscanf(req.Input, "text-%d", &idx)), and if err != nil or n != 1 call
t.Fatalf or t.Errorf with a clear message including req.Input and the error so
the test fails loudly instead of leaving idx as 0.
---
Nitpick comments:
In `@internal/integrations/ai/embedder.go`:
- Around line 184-194: The loop in internal/integrations/ai/embedder.go
unnecessarily re-shadows loop variables with `i, text := i, text`; since Go
1.22+ (we're on 1.24) the loop creates fresh variables per iteration, so remove
the `i, text := i, text` line and let the goroutine closure use the existing `i`
and `text`; keep the body calling `e.Embed(ctx, text)`, handling `err` and
assigning into `embeddings[i]` as before (inside the `g.Go` function).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: af70d79c-f55e-480d-9604-f7820c623136
📒 Files selected for processing (3)
go.modinternal/integrations/ai/embedder.gointernal/integrations/ai/embedder_test.go
| var idx int | ||
| fmt.Sscanf(req.Input, "text-%d", &idx) |
There was a problem hiding this comment.
Handle fmt.Sscanf error to satisfy linter and prevent silent failures.
Static analysis flags the unchecked return value. If parsing fails, idx remains 0, which could cause misleading test failures.
🛠️ Proposed fix
var idx int
- fmt.Sscanf(req.Input, "text-%d", &idx)
+ if _, err := fmt.Sscanf(req.Input, "text-%d", &idx); err != nil {
+ http.Error(w, "invalid input format", http.StatusBadRequest)
+ return
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var idx int | |
| fmt.Sscanf(req.Input, "text-%d", &idx) | |
| var idx int | |
| if _, err := fmt.Sscanf(req.Input, "text-%d", &idx); err != nil { | |
| http.Error(w, "invalid input format", http.StatusBadRequest) | |
| return | |
| } |
🧰 Tools
🪛 GitHub Check: Lint
[failure] 123-123:
Error return value of fmt.Sscanf is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/integrations/ai/embedder_test.go` around lines 122 - 123, The
fmt.Sscanf call that parses req.Input into idx must have its return values
checked to avoid silent failures; change the call in embedder_test.go to capture
the scan count and error (e.g. n, err := fmt.Sscanf(req.Input, "text-%d",
&idx)), and if err != nil or n != 1 call t.Fatalf or t.Errorf with a clear
message including req.Input and the error so the test fails loudly instead of
leaving idx as 0.
|
@simili-bot Review required Some checks were not successful failing checks Lint seems to be failling and code rabbit has added another comment as well, fix about and push to the same PR. |
|
Claude finished @Kavirubc's task in 1m 1s —— View job Fixing Lint Failure & CodeRabbit Comment
Changes (commit
|
…apture - Handle fmt.Sscanf return value in TestEmbedBatch_ResultsPreserveOrder to satisfy errcheck linter and fail loudly on malformed input - Remove i, text := i, text shadowing in EmbedBatch; Go 1.22+ creates fresh loop variables per iteration so the capture is unnecessary Co-authored-by: Kaviru Hapuarachchi <Kavirubc@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/integrations/ai/embedder.go (1)
22-23: Make the concurrency cap tunable.A package constant hard-codes the same limit for every provider and deployment. Moving this to
Embedderconfiguration would make rate-limit tuning and perf experiments possible without another code change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/integrations/ai/embedder.go` around lines 22 - 23, The hard-coded maxBatchConcurrency constant should be made configurable on the Embedder so different providers/deployments can tune concurrency; add a field (e.g., MaxBatchConcurrency int) to the Embedder struct with a sensible default (10) applied in the Embedder constructor/initializer, remove or deprecate the package-level const maxBatchConcurrency, and update any uses in EmbedBatch (and related methods) to read e.MaxBatchConcurrency instead of the constant; ensure validation (non-zero, positive) when constructing the Embedder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/integrations/ai/embedder.go`:
- Around line 181-196: Stop enqueuing new goroutines once the errgroup context
is canceled by checking the derived context before calling g.Go in the loop:
after obtaining g, ctx := errgroup.WithContext(ctx) and
g.SetLimit(maxBatchConcurrency), inside the for i, text := range texts loop test
if ctx.Err() != nil (or select on ctx.Done()) and break/return early to avoid
queuing canceled work; also capture loop variables (use i, text := i, text
inside the loop) so the closure passed to g.Go references the correct values
when invoking e.Embed(ctx, text) and writing embeddings[i].
---
Nitpick comments:
In `@internal/integrations/ai/embedder.go`:
- Around line 22-23: The hard-coded maxBatchConcurrency constant should be made
configurable on the Embedder so different providers/deployments can tune
concurrency; add a field (e.g., MaxBatchConcurrency int) to the Embedder struct
with a sensible default (10) applied in the Embedder constructor/initializer,
remove or deprecate the package-level const maxBatchConcurrency, and update any
uses in EmbedBatch (and related methods) to read e.MaxBatchConcurrency instead
of the constant; ensure validation (non-zero, positive) when constructing the
Embedder.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5de49180-9d9a-4bc9-9500-35ebab699661
📒 Files selected for processing (2)
internal/integrations/ai/embedder.gointernal/integrations/ai/embedder_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/integrations/ai/embedder_test.go
| g, ctx := errgroup.WithContext(ctx) | ||
| g.SetLimit(maxBatchConcurrency) | ||
|
|
||
| for i, text := range texts { | ||
| embedding, err := e.Embed(ctx, text) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to embed text %d: %w", i, err) | ||
| } | ||
| embeddings[i] = embedding | ||
| g.Go(func() error { | ||
| embedding, err := e.Embed(ctx, text) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to embed text %d: %w", i, err) | ||
| } | ||
| embeddings[i] = embedding | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| if err := g.Wait(); err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n internal/integrations/ai/embedder.go | head -220 | tail -60Repository: similigh/simili-bot
Length of output: 2061
🏁 Script executed:
cat -n internal/integrations/ai/embedder.go | head -210 | tail -80Repository: similigh/simili-bot
Length of output: 2724
🏁 Script executed:
wc -l internal/integrations/ai/embedder.goRepository: similigh/simili-bot
Length of output: 103
🏁 Script executed:
# Check the Go version requirement and imports
head -20 internal/integrations/ai/embedder.goRepository: similigh/simili-bot
Length of output: 441
🏁 Script executed:
# Search for other uses of errgroup in the codebase to see patterns
rg "errgroup\." --type go -B 2 -A 5Repository: similigh/simili-bot
Length of output: 567
🏁 Script executed:
# Check if there are any tests for EmbedBatch to understand expected behavior
find . -name "*test.go" -type f | xargs grep -l "EmbedBatch" 2>/dev/nullRepository: similigh/simili-bot
Length of output: 106
🏁 Script executed:
cat -n internal/integrations/ai/embedder_test.goRepository: similigh/simili-bot
Length of output: 8732
🏁 Script executed:
rg "maxBatchConcurrency" internal/integrations/ai/embedder.go -B 2 -A 2Repository: similigh/simili-bot
Length of output: 654
🏁 Script executed:
# Check for Go version constraints in go.mod
head -5 go.modRepository: similigh/simili-bot
Length of output: 122
🏁 Script executed:
# Look at errgroup usage patterns to understand the expected behavior better
# Also check if there's documentation or comments about context handling
rg "Wait\(\)" internal/integrations/ai/embedder.go -B 5 -A 5Repository: similigh/simili-bot
Length of output: 196
🏁 Script executed:
# Look for any race detector runs or tests
find . -name "*test.go" -type f -exec grep -l "race" {} \;Repository: similigh/simili-bot
Length of output: 216
Stop queueing new embeds once the errgroup context is canceled.
g.Go() blocks when SetLimit is full and does not watch the derived context. After the first failure or an upstream cancellation, this loop continues walking the rest of texts and enqueuing canceled work, which makes large failing batches return much more slowly than necessary.
Suggested fix
- g, ctx := errgroup.WithContext(ctx)
+ g, groupCtx := errgroup.WithContext(ctx)
g.SetLimit(maxBatchConcurrency)
+ stoppedEarly := false
for i, text := range texts {
+ if groupCtx.Err() != nil {
+ stoppedEarly = true
+ break
+ }
g.Go(func() error {
- embedding, err := e.Embed(ctx, text)
+ embedding, err := e.Embed(groupCtx, text)
if err != nil {
return fmt.Errorf("failed to embed text %d: %w", i, err)
}
embeddings[i] = embedding
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
+ if stoppedEarly && ctx.Err() != nil {
+ return nil, ctx.Err()
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| g, ctx := errgroup.WithContext(ctx) | |
| g.SetLimit(maxBatchConcurrency) | |
| for i, text := range texts { | |
| embedding, err := e.Embed(ctx, text) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to embed text %d: %w", i, err) | |
| } | |
| embeddings[i] = embedding | |
| g.Go(func() error { | |
| embedding, err := e.Embed(ctx, text) | |
| if err != nil { | |
| return fmt.Errorf("failed to embed text %d: %w", i, err) | |
| } | |
| embeddings[i] = embedding | |
| return nil | |
| }) | |
| } | |
| if err := g.Wait(); err != nil { | |
| return nil, err | |
| g, groupCtx := errgroup.WithContext(ctx) | |
| g.SetLimit(maxBatchConcurrency) | |
| stoppedEarly := false | |
| for i, text := range texts { | |
| if groupCtx.Err() != nil { | |
| stoppedEarly = true | |
| break | |
| } | |
| g.Go(func() error { | |
| embedding, err := e.Embed(groupCtx, text) | |
| if err != nil { | |
| return fmt.Errorf("failed to embed text %d: %w", i, err) | |
| } | |
| embeddings[i] = embedding | |
| return nil | |
| }) | |
| } | |
| if err := g.Wait(); err != nil { | |
| return nil, err | |
| } | |
| if stoppedEarly && ctx.Err() != nil { | |
| return nil, ctx.Err() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/integrations/ai/embedder.go` around lines 181 - 196, Stop enqueuing
new goroutines once the errgroup context is canceled by checking the derived
context before calling g.Go in the loop: after obtaining g, ctx :=
errgroup.WithContext(ctx) and g.SetLimit(maxBatchConcurrency), inside the for i,
text := range texts loop test if ctx.Err() != nil (or select on ctx.Done()) and
break/return early to avoid queuing canceled work; also capture loop variables
(use i, text := i, text inside the loop) so the closure passed to g.Go
references the correct values when invoking e.Embed(ctx, text) and writing
embeddings[i].
Simili Triage ReportNote Quality Score: 8.8/10 (Good) Classification
Quality Improvements
Similar Threads
Warning Possible Duplicate (Confidence: 99%) ⏳ This pull request will be automatically closed in 72 hours if no objections are raised. If you believe this is not a duplicate, please leave a comment explaining why. Generated by Simili Bot |
Simili Triage ReportNote Quality Score: 8.8/10 (Good) Classification
Quality Improvements
Similar Threads
Warning Possible Duplicate (Confidence: 99%) ⏳ This pull request will be automatically closed in 72 hours if no objections are raised. If you believe this is not a duplicate, please leave a comment explaining why. Generated by Simili Bot |
🧪 E2E Test✅ Bot responded: yes | Auto-closer (dry-run) | processed: 0 closed: 0 grace: 0 human: 0 | Test repo → gh-simili-bot/simili-e2e-23123710729 Auto-generated by E2E pipeline |
Refactor
EmbedBatchininternal/integrations/ai/embedder.goto process embedding requests concurrently instead of serially.A semaphore channel caps in-flight goroutines at
maxBatchConcurrency(10) to avoid overwhelming the API. Results are written to pre-indexed slice positions so no mutex is required. Context cancellation propagates viaerrgroup.WithContext.Also adds four unit tests covering empty input, order preservation, error propagation, and the concurrency cap.
Closes #51
Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores