Fast, explainable book metadata matching with pluggable sources
Project description
book-match
Fast, explainable book metadata matching with pluggable sources.
Features
- Fast: RapidFuzz-powered string matching (8x faster than alternatives)
- Explainable: Human-readable explanations for every match decision
- Pluggable Sources: Query Google Books, OpenLibrary, or add your own
- Batch Processing: Deduplicate and link datasets with blocking strategies
- Domain-Aware: Understands book titles, authors, ISBNs, editions, series
- Type-Safe: Full type hints with runtime validation
Installation
# Core library (matching only)
pip install book-match
# With metadata source integrations
pip install book-match[sources]
# Everything
pip install book-match[all]
Quick Start
Basic Matching
from book_match import Book, BookMatcher
# Create books to compare
local_book = Book(
title="The Great Gatsby",
authors=("F. Scott Fitzgerald",),
year=1925,
)
remote_book = Book(
title="Great Gatsby, The: A Novel",
authors=("Fitzgerald, F. Scott",),
isbn_13="9780743273565",
year=1925,
)
# Match them
matcher = BookMatcher()
result = matcher.match(local_book, remote_book)
print(f"Confidence: {result.confidence:.0%}") # 94%
print(f"Verdict: {result.verdict}") # MatchVerdict.AUTO_ACCEPT
print(f"Explanation: {result.explanation}")
# "Strong match (94% confidence). Title excellent match after subtitle
# handling: "The Great Gatsby" <-> "Great Gatsby, The: A Novel". Author
# excellent match: "F. Scott Fitzgerald" <-> "Fitzgerald, F. Scott"..."
Fast Filtering with quick_score
When you need to filter candidates before full matching:
matcher = BookMatcher()
# Returns just the confidence float, no explanation overhead
score = matcher.quick_score(local_book, remote_book)
if score > 0.7:
full_result = matcher.match(local_book, remote_book)
With Metadata Sources
import asyncio
from book_match import Book, BookResolver, OpenLibrarySource, GoogleBooksSource
async def find_book():
# Create resolver with multiple sources
async with BookResolver(
sources=[OpenLibrarySource(), GoogleBooksSource()]
) as resolver:
# Your book with partial metadata
my_book = Book(
title="Dune",
authors=("Frank Herbert",),
)
# Find matches across all sources
matches = await resolver.resolve(my_book, min_confidence=0.8)
for match in matches[:3]:
print(f"{match.confidence:.0%}: {match.remote_book.title}")
print(f" ISBN: {match.remote_book.isbn_13}")
print(f" Source: {match.remote_book.source}")
asyncio.run(find_book())
Resolve Strategies
Control how multi-source results are combined:
from book_match import BookResolver, ResolveStrategy
# Best match across all sources (default)
resolver = BookResolver(sources=sources, strategy=ResolveStrategy.BEST_MATCH)
# Return as soon as one source finds a confident match
resolver = BookResolver(sources=sources, strategy=ResolveStrategy.FIRST_CONFIDENT)
# Query all sources, return all results
resolver = BookResolver(sources=sources, strategy=ResolveStrategy.ALL_SOURCES)
# Require agreement between multiple sources
resolver = BookResolver(
sources=sources,
strategy=ResolveStrategy.CONSENSUS,
min_agreeing_sources=2,
)
Batch Deduplication
from book_match import Book, BatchMatcher
# Your library of books
books = [
Book(title="The Hobbit", authors=("J.R.R. Tolkien",)),
Book(title="Hobbit, The", authors=("Tolkien, J. R. R.",)),
Book(title="Lord of the Rings", authors=("Tolkien",)),
# ... thousands more
]
# Find duplicates
batch = BatchMatcher()
for duplicate in batch.deduplicate(books):
print(f"Duplicate found ({duplicate.confidence:.0%}):")
print(f" {duplicate.local_book.title}")
print(f" {duplicate.remote_book.title}")
Dataset Linking
Match books between two datasets:
from book_match import BatchMatcher
# Link your catalog against a reference dataset
batch = BatchMatcher()
for match in batch.link(my_catalog, reference_dataset):
print(f"Linked ({match.confidence:.0%}):")
print(f" Local: {match.local_book.title}")
print(f" Remote: {match.remote_book.title}")
Core Concepts
Books
The Book dataclass represents book metadata:
from book_match import Book
book = Book(
title="The Lord of the Rings: The Fellowship of the Ring",
authors=("J.R.R. Tolkien",),
isbn_10="0618346252",
isbn_13="9780618346257",
language="en",
year=1954,
publisher="Houghton Mifflin",
source="google_books", # Where this data came from
source_id="abc123", # ID in that source
)
Match Results
Every match returns detailed, explainable results:
result = matcher.match(book1, book2)
# Overall confidence (0.0 to 1.0)
result.confidence # 0.87
# Verdict: AUTO_ACCEPT, REVIEW, or REJECT
result.verdict # MatchVerdict.REVIEW
# Match classification
result.kind # MatchKind.SAME_WORK
# Human-readable explanation
result.explanation # "Possible match (87% confidence)..."
# Individual factors
for factor in result.factors:
print(f"{factor.name}: {factor.similarity:.0%}")
print(f" Weight: {factor.weight}")
print(f" Details: {factor.details}")
Match Kinds
Matches are classified by type:
from book_match import MatchKind
MatchKind.SAME_EDITION # Same book, same edition
MatchKind.SAME_WORK # Same book, different edition
MatchKind.UNCERTAIN # Cannot determine edition relationship
Configuration
Customize matching behavior:
from book_match import BookMatcher, MatchConfig
# Strict matching (fewer false positives)
strict_matcher = BookMatcher(MatchConfig.strict())
# Lenient matching (fewer false negatives)
lenient_matcher = BookMatcher(MatchConfig.lenient())
# ISBN-only matching
isbn_matcher = BookMatcher(MatchConfig.isbn_only())
# Custom configuration
custom_config = MatchConfig(
title_weight=0.6,
author_weight=0.3,
auto_accept_threshold=0.95,
strip_subtitles=True,
)
ISBN Handling
Proper ISBN validation with checksums:
from book_match import (
InvalidISBNError,
is_valid_isbn,
is_valid_isbn10,
is_valid_isbn13,
validate_isbn,
isbn10_to_isbn13,
isbn13_to_isbn10,
normalize_isbn,
normalize_to_isbn13,
extract_isbns,
compare_isbns,
)
# Validation (checks checksum!)
is_valid_isbn("9780306406157") # True
is_valid_isbn("1234567890") # False (invalid checksum)
# Specific format validation
is_valid_isbn10("0306406152") # True
is_valid_isbn13("9780306406157") # True
# Returns normalized ISBN string on success
normalized = validate_isbn("0-306-40615-2") # "0306406152"
# Raises InvalidISBNError on failure
try:
validate_isbn("1234567890")
except InvalidISBNError as e:
print(e) # "Invalid ISBN: 1234567890 (invalid ISBN-10 checksum)"
# Conversion
isbn10_to_isbn13("0306406152") # "9780306406157"
isbn13_to_isbn10("9780306406157") # "0306406152"
normalize_to_isbn13("0306406152") # "9780306406157" (auto-detects format)
# Comparison (handles format differences)
compare_isbns("0306406152", "9780306406157") # True (same book)
# Extract from text
text = "ISBN: 978-0-306-40615-7 and also 0-306-40615-2"
extract_isbns(text) # ["9780306406157", "0306406152"]
Custom Metadata Sources
Add your own sources by extending BaseSource:
from book_match import BaseSource, Book, SearchQuery
class MyLibrarySource(BaseSource):
@property
def name(self) -> str:
return "my_library"
async def search(self, query: SearchQuery, limit: int = 10) -> list[Book]:
# Query your database/API
results = await my_api.search(
title=query.title,
author=query.authors[0] if query.authors else None,
)
return [
Book(
title=r["title"],
authors=tuple(r["authors"]),
isbn_13=r.get("isbn"),
source=self.name,
source_id=r["id"],
)
for r in results
]
async def fetch_by_isbn(self, isbn: str) -> Book | None:
result = await my_api.get_by_isbn(isbn)
if result:
return Book(...)
return None
# Use it
resolver = BookResolver(sources=[MyLibrarySource()])
You can also implement the MetadataSource protocol directly if you prefer structural typing over inheritance.
Batch Processing
For large datasets, use blocking to avoid O(n^2) comparisons:
from book_match import (
BatchMatcher,
BatchConfig,
TitlePrefix,
TitleFirstWord,
FirstAuthorSurname,
ISBN13Prefix,
YearRange,
LanguageBlock,
CompositeBlock,
)
# Custom blocking rules
batch = BatchMatcher(
blocking_rules=[
TitlePrefix(4), # Group by first 4 chars of title
FirstAuthorSurname(), # Group by author surname
],
batch_config=BatchConfig(
min_confidence=0.7,
max_results_per_book=5,
),
)
# Deduplicate with progress
def on_progress(progress):
print(f"{progress.percent_complete:.1f}% - {progress.matches_found} matches")
duplicates = list(batch.deduplicate(books, on_progress=on_progress))
Available Blocking Rules
| Rule | Description |
|---|---|
TitlePrefix(n) |
Groups by first n characters of the title |
TitleFirstWord() |
Groups by the first significant word (skips articles) |
FirstAuthorSurname() |
Groups by the first author's surname |
ISBN13Prefix(n) |
Groups by ISBN-13 prefix (default 7 = same publisher) |
YearRange(n) |
Groups by year range (default 5 years) |
LanguageBlock() |
Groups by language code |
CompositeBlock(rules) |
Combines multiple rules into a single blocking key |
Preset Configurations
from book_match import BatchConfig
# Fast processing (lower quality, more speed)
batch = BatchMatcher(batch_config=BatchConfig.fast())
# Thorough processing (higher quality, more comparisons)
batch = BatchMatcher(batch_config=BatchConfig.thorough())
Performance
Benchmarks on a MacBook Pro M2:
| Operation | book-match | Alternative |
|---|---|---|
| Single match | ~50us | ~400us (jellyfish) |
| 10k deduplication | ~2s | ~15s (naive) |
| ISBN validation | ~1us | ~5us (isbnlib) |
API Reference
Core Types
Book- Immutable book metadataMatchResult- Complete match result with explanationMatchFactor- Individual scoring factorMatchVerdict- AUTO_ACCEPT, REVIEW, REJECTMatchKind- SAME_EDITION, SAME_WORK, UNCERTAINSearchQuery- Query object for metadata sourcesBatchProgress- Progress tracking for batch operations
Configuration
MatchConfig- Matching engine configuration (with.strict(),.lenient(),.isbn_only()presets)BatchConfig- Batch processing configuration (with.fast(),.thorough()presets)SourceConfig- Metadata source configuration (timeouts, retries, caching)
Matching
BookMatcher- Core matching engine.match()- Full match with explanation.match_many()- Match against multiple candidates.quick_score()- Fast confidence score (no explanation overhead)
Sources
BookResolver- Multi-source orchestrator with strategy supportGoogleBooksSource- Google Books APIOpenLibrarySource- OpenLibrary.org APIBaseSource- Abstract base class for custom sourcesMetadataSource- Protocol for structural typingResolveStrategy- BEST_MATCH, FIRST_CONFIDENT, ALL_SOURCES, CONSENSUS
Batch Processing
BatchMatcher- Batch deduplication and dataset linking.deduplicate()- Find duplicates within a dataset.link()- Match between two datasets.find_matches()- Match a single book against candidates
BlockingRule- Abstract base class for blocking strategies- 7 built-in blocking rules (see Batch Processing section)
ISBN
is_valid_isbn(),is_valid_isbn10(),is_valid_isbn13()- Validationvalidate_isbn()- Validation with error reasonisbn10_to_isbn13(),isbn13_to_isbn10()- Conversionnormalize_isbn(),normalize_to_isbn13()- Normalizationcompare_isbns()- Format-agnostic comparisonextract_isbns()- Extract from text
Normalization
normalize_title(),normalize_author(),normalize_authors()- Domain-specific normalizationstrip_subtitle(),strip_series_markers(),extract_series_info()- Title processing
Similarity
jaro_similarity(),jaro_winkler_similarity()- Character-level similaritytoken_set_ratio(),token_sort_ratio()- Token-level similarityhybrid_similarity()- Blended similarity tuned for book titlesquick_ratio()- Fast pre-filter
Exceptions
BookMatchError- Base exceptionISBNError,InvalidISBNError- ISBN validation errorsSourceError,SourceNotFoundError,SourceRequestError,SourceRateLimitError- Source errorsConfigurationError,BatchError,BlockingError- Configuration and batch errors
Contributing
Contributions welcome! Please read our contributing guidelines first.
# Setup development environment
git clone https://github.com/bibliostack/book-match
cd book-match
pip install -e ".[dev]"
# Run tests
pytest
# Run linter
ruff check .
# Type checking
mypy src/
License
MIT License - see LICENSE for details.
Acknowledgments
- RapidFuzz for fast string matching
- OpenLibrary for free book metadata
- Splink for record linkage inspiration
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file book_match-1.2.1.tar.gz.
File metadata
- Download URL: book_match-1.2.1.tar.gz
- Upload date:
- Size: 62.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7d34da8f352c7a41e06007d687a9812d5b76ff002b5e0eb158baaf745db1b29
|
|
| MD5 |
b4b0202383a599978f8c9ea8c70d848f
|
|
| BLAKE2b-256 |
533944abc751412193411f492fe494caab3d659669a3086efe272dc2ef096ee6
|
Provenance
The following attestation bundles were made for book_match-1.2.1.tar.gz:
Publisher:
publish.yml on bibliostack/book-match
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
book_match-1.2.1.tar.gz -
Subject digest:
d7d34da8f352c7a41e06007d687a9812d5b76ff002b5e0eb158baaf745db1b29 - Sigstore transparency entry: 1194135503
- Sigstore integration time:
-
Permalink:
bibliostack/book-match@850248c7e4b0315b99d713a7453fbf792235e6ba -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/bibliostack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@850248c7e4b0315b99d713a7453fbf792235e6ba -
Trigger Event:
release
-
Statement type:
File details
Details for the file book_match-1.2.1-py3-none-any.whl.
File metadata
- Download URL: book_match-1.2.1-py3-none-any.whl
- Upload date:
- Size: 55.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5bf00e011692aaa6ee385f1d3b30cbde43ca21385085818af453338da6aa80ae
|
|
| MD5 |
38041180a755c468b73f937d2bd023c0
|
|
| BLAKE2b-256 |
516af5d64d92a8896e18359b5cbd6c5d0e92e7a26ea78d1f8ea0412688f09212
|
Provenance
The following attestation bundles were made for book_match-1.2.1-py3-none-any.whl:
Publisher:
publish.yml on bibliostack/book-match
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
book_match-1.2.1-py3-none-any.whl -
Subject digest:
5bf00e011692aaa6ee385f1d3b30cbde43ca21385085818af453338da6aa80ae - Sigstore transparency entry: 1194135507
- Sigstore integration time:
-
Permalink:
bibliostack/book-match@850248c7e4b0315b99d713a7453fbf792235e6ba -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/bibliostack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@850248c7e4b0315b99d713a7453fbf792235e6ba -
Trigger Event:
release
-
Statement type: