From e7c426defbcc65ced041c732b7d92a71c8663f1b Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Wed, 16 Sep 2026 06:00:52 +0900 Subject: [PATCH 1/3] docs: make Go quick start context first --- README.md | 27 +++++++--- docs/content/getting-started/quick-start.md | 58 ++++++++++++++------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 82a1eb0..0d72025 100644 --- a/README.md +++ b/README.md @@ -41,31 +41,44 @@ Start Redis locally, then: package main import ( + "context" "fmt" + "log" + "time" "github.com/skyoo2003/acor/pkg/acor" ) func main() { - ac, err := acor.Create(&acor.AhoCorasickArgs{ + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ac, err := acor.CreateContext(ctx, &acor.AhoCorasickArgs{ Addr: "localhost:6379", Name: "sample", Preset: acor.PresetBalanced, }) if err != nil { - panic(err) + return fmt.Errorf("create collection: %w", err) } - defer ac.Close() + defer func() { _ = ac.Close() }() - if _, err := ac.AddMany([]string{"he", "her", "him"}, nil); err != nil { - panic(err) + if _, err := ac.AddManyContext(ctx, []string{"he", "her", "him"}, nil); err != nil { + return fmt.Errorf("add keywords: %w", err) } - matches, err := ac.Find("he is him") + matches, err := ac.FindMatchesContext(ctx, "he is him", nil) if err != nil { - panic(err) + return fmt.Errorf("find matches: %w", err) } fmt.Println(matches) + return nil } ``` diff --git a/docs/content/getting-started/quick-start.md b/docs/content/getting-started/quick-start.md index c0176ad..2603f5f 100644 --- a/docs/content/getting-started/quick-start.md +++ b/docs/content/getting-started/quick-start.md @@ -10,33 +10,51 @@ weight: 2 package main import ( - "fmt" + "context" + "fmt" + "log" + "time" - "github.com/skyoo2003/acor/pkg/acor" + "github.com/skyoo2003/acor/pkg/acor" ) func main() { - ac, err := acor.Create(&acor.AhoCorasickArgs{ - Addr: "localhost:6379", - Name: "sample", - }) - if err != nil { - panic(err) - } - defer ac.Close() - - if _, err := ac.AddMany([]string{"he", "her", "him"}, nil); err != nil { - panic(err) - } - - matched, err := ac.Find("he is him") - if err != nil { - panic(err) - } - fmt.Println(matched) + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ac, err := acor.CreateContext(ctx, &acor.AhoCorasickArgs{ + Addr: "localhost:6379", + Name: "sample", + }) + if err != nil { + return fmt.Errorf("create collection: %w", err) + } + defer func() { _ = ac.Close() }() + + if _, err := ac.AddManyContext(ctx, []string{"he", "her", "him"}, nil); err != nil { + return fmt.Errorf("add keywords: %w", err) + } + + matched, err := ac.FindMatchesContext(ctx, "he is him", nil) + if err != nil { + return fmt.Errorf("find matches: %w", err) + } + fmt.Println(matched) + return nil } ``` +The setup context bounds construction I/O only. `Close` controls the instance +lifetime; use the `*Context` methods when an operation needs cancellation or a +timeout. The [API reference](../../reference/api/#creating-a-collection) documents +the full constructor and lifecycle contract. + ## Redis topologies Pick exactly one set of connection fields — mixing them returns From 55c900a2f2130a1b62bc792fb65c665d77aa6ab5 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Wed, 16 Sep 2026 06:02:33 +0900 Subject: [PATCH 2/3] docs: clarify Go API choices and errors --- docs/content/guides/batch-operations.md | 3 ++ docs/content/guides/parallel-matching.md | 3 ++ docs/content/reference/api.md | 42 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/docs/content/guides/batch-operations.md b/docs/content/guides/batch-operations.md index 1632478..b6a15fb 100644 --- a/docs/content/guides/batch-operations.md +++ b/docs/content/guides/batch-operations.md @@ -42,6 +42,9 @@ for _, ke := range result.Failed { shapes for `BatchResult` and `KeywordError` are in the [API reference](../../reference/api/#batchresult). +For sentinel and structured error handling, see the +[API error-handling guidance](../../reference/api/#error-handling). + ## Scanning many texts `FindMany` loads the automaton once and scans every text against that one snapshot, so it diff --git a/docs/content/guides/parallel-matching.md b/docs/content/guides/parallel-matching.md index da0f8df..73e3986 100644 --- a/docs/content/guides/parallel-matching.md +++ b/docs/content/guides/parallel-matching.md @@ -9,6 +9,9 @@ weight: 2 concurrently. The automaton is loaded once per call, so the Redis cost is the same as a serial `Find` however many chunks result. +For choosing between ordinary, indexed, streaming, and parallel operations, see the +[API selection table](../../reference/api/#choosing-a-matching-api). + ```go matches, err := ac.FindParallel(largeText, &acor.ParallelOptions{ diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index cf8c0de..fa06099 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -109,6 +109,24 @@ See [Batch Operations](../../guides/batch-operations/) for the modes and result All positions are **rune** offsets, not byte offsets. +## Choosing a matching API + +Use the narrowest operation that matches the input and result you need: + +| Need | Use | Result/constraint | +| --- | --- | --- | +| Know which keywords occur | `FindContext` | One entry per occurrence; use `FindSetContext` for unique keywords | +| Need every occurrence or spans | `FindMatchesContext` | `Match` values with rune `[Start, End)` spans and selectable match kind | +| Stop after the first match | `ContainsContext` | Boolean result | +| Scan an `io.Reader` | `FindStreamContext` | Incremental overlapping matches; callback `false` stops | +| Scan several bounded strings | `FindManyContext` | One loaded engine and results keyed by input text | +| Scan one large string concurrently | `FindParallelContext` / `FindIndexParallelContext` | Required positive `ChunkSize`; use `AutoOverlap` for boundary safety | +| Process bounded text with rewrite or work limits | `Scan`, `MaskText`, `ReplaceText` | V3/versioned text-processing APIs with explicit options | + +The non-context methods remain useful for callers that do not need per-operation +cancellation. Use the `*Context` form when the call may contact Redis or needs a +timeout or cancellation deadline. + ### FindMatches Every occurrence in scan order with its half-open rune span `[Start, End)`. The default @@ -215,6 +233,30 @@ type KeywordError struct { } ``` +## Error handling + +Use `errors.Is` for sentinel errors and `errors.As` for structured errors. Do +not match error strings: + + +```go +err := acor.ErrConcurrencyConflict // replace with the error returned by an operation +if errors.Is(err, acor.ErrConcurrencyConflict) { + // The library already exhausted its internal write retries. +} + +var redisErr *acor.RedisError +if errors.As(err, &redisErr) { + fmt.Printf("Redis operation %s failed for %q: %v\n", redisErr.Op, redisErr.Key, redisErr.Err) +} +``` + +For best-effort batches, the call returns a `BatchResult`; per-keyword failures +are in `result.Failed` and successful work remains committed. For transactional +batches, a failure returns an error after the batch is rolled back. An entry in +`result.Skipped` is not an error: it represents a duplicate add or an absent +remove. + ## Statistics `Info()` reads Redis; `CacheStats()` does not, so it is cheap to scrape on a timer. From 38e33f7bc2937c263f8260fc6dd2abdb8669d086 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Wed, 16 Sep 2026 06:03:43 +0900 Subject: [PATCH 3/3] examples: show context-aware library usage --- examples/basic/main.go | 21 +++++++++++++-------- examples/batch/main.go | 19 ++++++++++++------- examples/parallel/main.go | 17 +++++++++++------ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/examples/basic/main.go b/examples/basic/main.go index cc93561..07c0882 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -6,40 +6,45 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/skyoo2003/acor/pkg/acor" ) func main() { - ac, err := acor.Create(&acor.AhoCorasickArgs{ + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ac, err := acor.CreateContext(ctx, &acor.AhoCorasickArgs{ Addr: "localhost:6379", Name: "example-basic", }) if err != nil { - fmt.Fprintf(os.Stderr, "failed to create: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to create: %w", err)) return } defer func() { _ = ac.Close() }() keywords := []string{"he", "she", "his", "hers"} for _, kw := range keywords { - if _, addErr := ac.Add(kw); addErr != nil { - fmt.Fprintf(os.Stderr, "failed to add keyword: %v\n", addErr) + if _, addErr := ac.AddContext(ctx, kw); addErr != nil { + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to add keyword: %w", addErr)) return } } - matches, err := ac.Find("ushers") + matches, err := ac.FindContext(ctx, "ushers") if err != nil { - fmt.Fprintf(os.Stderr, "failed to find: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to find: %w", err)) return } fmt.Println(matches) - if err := ac.Flush(); err != nil { - fmt.Fprintf(os.Stderr, "failed to flush: %v\n", err) + if err := ac.FlushContext(ctx); err != nil { + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to flush: %w", err)) } } diff --git a/examples/batch/main.go b/examples/batch/main.go index 923bcb6..ca8ce72 100644 --- a/examples/batch/main.go +++ b/examples/batch/main.go @@ -6,35 +6,40 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/skyoo2003/acor/pkg/acor" ) func main() { - ac, err := acor.Create(&acor.AhoCorasickArgs{ + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ac, err := acor.CreateContext(ctx, &acor.AhoCorasickArgs{ Addr: "localhost:6379", Name: "example-batch", }) if err != nil { - fmt.Fprintf(os.Stderr, "failed to create: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to create: %w", err)) return } defer func() { _ = ac.Close() }() - result, err := ac.AddMany([]string{"foo", "bar", "baz"}, &acor.BatchOptions{ + result, err := ac.AddManyContext(ctx, []string{"foo", "bar", "baz"}, &acor.BatchOptions{ Mode: acor.BatchModeTransactional, }) if err != nil { - fmt.Fprintf(os.Stderr, "failed to add many: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to add many: %w", err)) return } - fmt.Printf("Added: %d, Failed: %d\n", len(result.Added), len(result.Failed)) + fmt.Printf("Added: %d, Failed: %d, Skipped: %d\n", len(result.Added), len(result.Failed), len(result.Skipped)) - matches, err := ac.FindMany([]string{"foo bar", "baz qux"}) + matches, err := ac.FindManyContext(ctx, []string{"foo bar", "baz qux"}) if err != nil { - fmt.Fprintf(os.Stderr, "failed to find many: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to find many: %w", err)) return } for text, m := range matches { diff --git a/examples/parallel/main.go b/examples/parallel/main.go index 42e9ada..aa11b22 100644 --- a/examples/parallel/main.go +++ b/examples/parallel/main.go @@ -5,38 +5,43 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/skyoo2003/acor/pkg/acor" ) func main() { - ac, err := acor.Create(&acor.AhoCorasickArgs{ + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ac, err := acor.CreateContext(ctx, &acor.AhoCorasickArgs{ Addr: "localhost:6379", Name: "example-parallel", }) if err != nil { - fmt.Fprintf(os.Stderr, "failed to create: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to create: %w", err)) return } defer func() { _ = ac.Close() }() - _, err = ac.AddMany([]string{"foo", "bar", "baz"}, nil) + _, err = ac.AddManyContext(ctx, []string{"foo", "bar", "baz"}, nil) if err != nil { - fmt.Fprintf(os.Stderr, "failed to add keywords: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to add keywords: %w", err)) return } largeText := "foo bar baz " - matches, err := ac.FindParallel(largeText, &acor.ParallelOptions{ + matches, err := ac.FindParallelContext(ctx, largeText, &acor.ParallelOptions{ Workers: 4, Boundary: acor.ChunkBoundaryWord, ChunkSize: 1000, AutoOverlap: true, }) if err != nil { - fmt.Fprintf(os.Stderr, "failed to find parallel: %v\n", err) + fmt.Fprintln(os.Stderr, fmt.Errorf("failed to find parallel: %w", err)) return }