Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand Down
58 changes: 38 additions & 20 deletions docs/content/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/content/guides/batch-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/content/guides/parallel-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<!-- doccheck -->
```go
matches, err := ac.FindParallel(largeText, &acor.ParallelOptions{
Expand Down
42 changes: 42 additions & 0 deletions docs/content/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

<!-- doccheck -->
```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.
Expand Down
21 changes: 13 additions & 8 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
19 changes: 12 additions & 7 deletions examples/batch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 11 additions & 6 deletions examples/parallel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down