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
76 changes: 44 additions & 32 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,48 @@ permissions:
contents: read

jobs:
test:
build-test:
strategy:
matrix:
runs-on: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
python-version: '3.13'
- name: Build package
uses: PyO3/maturin-action@v1
with:
manylinux: auto
args: -i ${{ matrix.python-version }} --release
args: --release
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: test-wheels-${{ matrix.runs-on }}
path: target/wheels

test:
needs: [ build-test ]
strategy:
matrix:
runs-on: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- uses: actions/download-artifact@v8
with:
name: test-wheels-${{ matrix.runs-on }}
path: test-wheels
- name: Test package
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install daachorse --no-index --find-links target/wheels --force-reinstall
pip install daachorse --no-index --find-links test-wheels --force-reinstall
mypy --strict tests
pytest

Expand All @@ -43,8 +64,8 @@ jobs:
needs: [ test ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Build package
Expand All @@ -63,23 +84,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
target: [ x86_64, i686 ]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v7
- name: Build package
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i ${{ matrix.python-version }} --release
args: --release
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-linux-${{ matrix.python-version }}-${{ matrix.target }}
name: wheels-linux-${{ matrix.target }}
path: target/wheels

pack-windows:
Expand All @@ -88,46 +105,41 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
target: [ x64, x86 ]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
python-version: '3.13'
architecture: ${{ matrix.target }}
- name: Build package
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: -i ${{ matrix.python-version }} --release
args: --release
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-windows-${{ matrix.python-version }}-${{ matrix.target }}
name: wheels-windows-${{ matrix.target }}
path: target/wheels

pack-macos:
if: "startsWith(github.ref, 'refs/tags/')"
needs: [ test ]
runs-on: macos-latest
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
python-version: '3.13'
- name: Build package
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: -i ${{ matrix.python-version }} --release --target universal2-apple-darwin
args: --release --target universal2-apple-darwin
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-macos-${{ matrix.python-version }}
name: wheels-macos
path: target/wheels

release:
Expand All @@ -143,6 +155,6 @@ jobs:
with:
pattern: wheels-*
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Publish to PyPI
run: uv publish wheels-*/*
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "python-daachorse"
version = "0.3.0"
version = "0.5.0"
edition = "2021"
authors = [
"Koichi Akabe <vbkaisetsu@gmail.com>",
Expand All @@ -16,8 +16,8 @@ name = "daachorse"
crate-type = ["cdylib"]

[dependencies]
daachorse = "3.0.0" # MIT or Apache-2.0
pyo3 = { version = "0.28.3", features = ["extension-module"] } # Apache-2.0
daachorse = "5.0.0" # MIT or Apache-2.0
pyo3 = { version = "0.29.2", features = ["extension-module", "abi3-py310"] } # Apache-2.0

[profile.release]
lto = true
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ To build an automaton for strings, use `CharwiseDoubleArrayAhoCorasick` instead.
[(0, 3, 0), (4, 5, 2)]
```

### Profile-Guided Optimization (PGO)

The double array constructed by daachorse is designed to improve CPU cache efficiency. However, it
is not always the optimal layout for every document being scanned.

daachorse addresses this issue by supporting Profile-Guided Optimization (PGO). By providing a
corpus when constructing the automaton, the memory layout is optimized, which may improve
performance on documents similar to the given corpus.

```python
>>> import daachorse
>>> patterns = [b'ab', b'a', b'abcd']
>>> pma = daachorse.DoubleArrayAhoCorasick(patterns, corpus=[b'abcd'])
```

## License

Licensed under either of
Expand Down
4 changes: 2 additions & 2 deletions daachorse.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MATCH_KIND_LEFTMOST_LONGEST: int
MATCH_KIND_STANDARD: int

class DoubleArrayAhoCorasick:
def __init__(self, patterns: list[bytes], match_kind: int = 0) -> None: ...
def __init__(self, patterns: list[bytes], /, match_kind: int = 0, corpus: list[bytes] | None = None, use_prefilter: bool = True) -> None: ...
def find(self, haystack: bytes) -> list[tuple[int, int, int]]: ...
def find_overlapping(self, haystack: bytes) -> list[tuple[int, int, int]]: ...
def find_overlapping_no_suffix(self, haystack: bytes) -> list[tuple[int, int, int]]: ...
Expand All @@ -12,7 +12,7 @@ class DoubleArrayAhoCorasick:
def deserialize(data: bytes) -> "DoubleArrayAhoCorasick": ...

class CharwiseDoubleArrayAhoCorasick:
def __init__(self, patterns: list[str], match_kind: int = 0) -> None: ...
def __init__(self, patterns: list[str], /, match_kind: int = 0, corpus: list[str] | None = None, use_prefilter: bool = True) -> None: ...
def find(self, haystack: str) -> list[tuple[int, int, int]]: ...
def find_overlapping(self, haystack: str) -> list[tuple[int, int, int]]: ...
def find_overlapping_no_suffix(self, haystack: str) -> list[tuple[int, int, int]]: ...
Expand Down
16 changes: 16 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,19 @@ To build an automaton for strings, use ``CharwiseDoubleArrayAhoCorasick`` instea
>>> pma = daachorse.CharwiseDoubleArrayAhoCorasick(patterns)
>>> pma.find('全世界中に')
[(0, 3, 0), (4, 5, 2)]

Profile-Guided Optimization (PGO)
---------------------------------

The double array constructed by daachorse is designed to improve CPU cache efficiency. However, it
is not always the optimal layout for every document being scanned.

daachorse addresses this issue by supporting Profile-Guided Optimization (PGO). By providing a
corpus when constructing the automaton, the memory layout is optimized, which may improve
performance on documents similar to the given corpus.

.. code-block:: python

>>> import daachorse
>>> patterns = [b'ab', b'a', b'abcd']
>>> pma = daachorse.DoubleArrayAhoCorasick(patterns, corpus=[b'abcd'])
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ build-backend = "maturin"

[project]
name = "daachorse"
version = "0.3.0"
version = "0.5.0"
description = "🐎 A fast implementation of the Aho-Corasick algorithm using the compact double-array data structure"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"License :: OSI Approved :: Apache Software License",
"License :: OSI Approved :: MIT License",
Expand Down
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pytest>=7.2.1
mypy>=0.991
pytest>=9.1.1
mypy>=2.3.0
64 changes: 54 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ use pyo3::{
///
/// :param patterns: List of bytes patterns.
/// :param match_kind: A search option of the Aho-Corasick automaton.
/// :param corpus: List of bytes to be used as a corpus for the profile-guided optimization.
/// :param use_prefilter: Whether to use a prefilter.
/// :type patterns: list[bytes]
/// :type match_kind: int
/// :type corpus: list[bytes] | None
/// :type use_prefilter: bool
/// :rtype: daachorse.DoubleArrayAhoCorasick
#[pyclass(module = "daachorse")]
struct DoubleArrayAhoCorasick {
Expand All @@ -26,17 +30,35 @@ struct DoubleArrayAhoCorasick {
#[pymethods]
impl DoubleArrayAhoCorasick {
#[new]
#[pyo3(signature = (patterns, /, match_kind = 0))]
fn new(py: Python, patterns: Vec<Py<PyBytes>>, match_kind: u8) -> PyResult<Self> {
let raw_patterns: PyResult<Vec<Vec<u8>>> =
patterns.iter().map(|pat| pat.extract(py)).collect();
let raw_patterns = raw_patterns?;
#[pyo3(signature = (patterns, /, match_kind = 0, corpus = None, use_prefilter = true))]
fn new(
py: Python,
patterns: Vec<Py<PyBytes>>,
match_kind: u8,
corpus: Option<Vec<Py<PyBytes>>>,
use_prefilter: bool,
) -> PyResult<Self> {
Comment thread
vbkaisetsu marked this conversation as resolved.
let raw_patterns = patterns
.iter()
.map(|pat| pat.extract(py))
.collect::<PyResult<Vec<Vec<u8>>>>()?;
let match_kind = ::daachorse::MatchKind::from(match_kind);
let raw_corpus = corpus.map_or_else(
|| Ok(vec![]),
|corpus| {
corpus
.iter()
.map(|c| c.extract(py))
.collect::<PyResult<Vec<Vec<u8>>>>()
},
)?;
Ok(Self {
pma: py
.detach(|| {
::daachorse::DoubleArrayAhoCorasickBuilder::new()
.match_kind(match_kind)
.corpus(raw_corpus)
.use_prefilter(use_prefilter)
.build(raw_patterns)
Comment thread
vbkaisetsu marked this conversation as resolved.
})
.map_err(|e| PyValueError::new_err(e.to_string()))?,
Expand Down Expand Up @@ -249,8 +271,12 @@ impl DoubleArrayAhoCorasick {
///
/// :param patterns: List of string patterns.
/// :param match_kind: A search option of the Aho-Corasick automaton.
/// :param corpus: List of strings to be used as a corpus for the profile-guided optimization.
/// :param use_prefilter: Whether to use a prefilter.
/// :type patterns: list[str]
/// :type match_kind: int
/// :type corpus: list[str] | None
/// :type use_prefilter: bool
/// :rtype: daachorse.CharwiseDoubleArrayAhoCorasick
#[pyclass(module = "daachorse")]
struct CharwiseDoubleArrayAhoCorasick {
Expand All @@ -260,17 +286,35 @@ struct CharwiseDoubleArrayAhoCorasick {
#[pymethods]
impl CharwiseDoubleArrayAhoCorasick {
#[new]
#[pyo3(signature = (patterns, /, match_kind = 0))]
fn new(py: Python, patterns: Vec<Py<PyString>>, match_kind: u8) -> PyResult<Self> {
let raw_patterns: PyResult<Vec<String>> =
patterns.iter().map(|pat| pat.extract(py)).collect();
let raw_patterns = raw_patterns?;
#[pyo3(signature = (patterns, /, match_kind = 0, corpus = None, use_prefilter = true))]
fn new(
py: Python,
patterns: Vec<Py<PyString>>,
match_kind: u8,
corpus: Option<Vec<Py<PyString>>>,
use_prefilter: bool,
) -> PyResult<Self> {
let raw_patterns = patterns
.iter()
.map(|pat| pat.extract(py))
.collect::<PyResult<Vec<String>>>()?;
let match_kind = ::daachorse::MatchKind::from(match_kind);
let raw_corpus = corpus.map_or_else(
|| Ok(vec![]),
|corpus| {
corpus
.iter()
.map(|c| c.extract(py))
.collect::<PyResult<Vec<String>>>()
},
)?;
Ok(Self {
pma: py
.detach(|| {
::daachorse::CharwiseDoubleArrayAhoCorasickBuilder::new()
.match_kind(match_kind)
.corpus(raw_corpus)
.use_prefilter(use_prefilter)
.build(raw_patterns)
})
.map_err(|e| PyValueError::new_err(e.to_string()))?,
Expand Down
26 changes: 26 additions & 0 deletions tests/test_bytewise.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,32 @@ def test_find_overlapping_invalid_option() -> None:
with pytest.raises(ValueError):
pma.find_overlapping(haystack)


def test_corpus() -> None:
haystack = b'this is a pen'
patterns = [b't', b'hi', b'h', b'this', b'pe']
corpus = [b'this is a pen', b'that is a pencil']
pma = daachorse.DoubleArrayAhoCorasick(patterns, corpus=corpus)

assert [
(0, 1, 0),
(1, 2, 2),
(10, 12, 4),
] == pma.find(haystack)


def test_use_prefilter_disabled() -> None:
haystack = b'this is a pen'
patterns = [b't', b'hi', b'h', b'this', b'pe']
pma = daachorse.DoubleArrayAhoCorasick(patterns, use_prefilter=False)

assert [
(0, 1, 0),
(1, 2, 2),
(10, 12, 4),
] == pma.find(haystack)


def test_serialization() -> None:
haystack = b'this is a pen'
patterns = [b't', b'hi', b'h', b'this', b'pe']
Expand Down
Loading