lmz/phpanalysis
A PHP Chinese word segmentation library based on PHPAnalysis. It splits Chinese text into words using dictionary rules, supports UTF-8/GBK input, and is useful for keyword extraction, search indexing, and basic text analysis in PHP projects.
lmz/phpanalysis is a Composer wrapper around the classic single-class PhpAnalysis Chinese word segmentation library (中文分词). It is not an NLP toolkit — it does dictionary-based segmentation (forward/backward maximum matching), simplified/traditional conversion, and a rough keyword extractor. Treat it as "one class + one dictionary file."
Minimal setup:
composer require lmz/phpanalysis
find vendor/lmz/phpanalysis -maxdepth 2 -type f | head -50
You are looking for two things and nothing else matters: the class file (legacy PhpAnalysis or a namespaced variant under src/) and the dictionary (dict/, dic.txt, dict.txt). Confirm what actually autoloads before writing code:
require 'vendor/autoload.php';
var_dump(class_exists('PhpAnalysis'), class_exists('Lmz\PhpAnalysis\PhpAnalysis'));
First use case — segmenting a product title to build search terms:
$pa = new PhpAnalysis('utf-8', 'utf-8', true); // last arg = extract keywords
$pa->LoadDict();
$pa->SetSource('小米手机官方旗舰店正品保障');
$pa->StartAnalysis();
$words = $pa->GetFinallyResult(); // ['小米','手机','官方','旗舰店',...]
$keywords = $pa->GetFinallyKeywords(5); // top-N by the library's internal weighting
If the snippet fails on method names, open the class file and read SetSource / StartAnalysis / GetFinallyResult — the whole library is ~a few hundred lines and reading it once is faster than fighting it.
Load the dictionary once, segment many. Dictionary parsing is the dominant cost. Put the instance behind a singleton, not a new per call.
// app/Support/ChineseSegmenter.php
class ChineseSegmenter
{
private PhpAnalysis $pa;
public function __construct()
{
$this->pa = new PhpAnalysis('utf-8', 'utf-8', true);
$this->pa->LoadDict();
}
public function segment(string $text): array
{
$this->pa->SetSource($text);
$this->pa->StartAnalysis();
return $this->pa->GetFinallyResult();
}
}
// AppServiceProvider: $this->app->singleton(ChineseSegmenter::class);
Cache the parsed dictionary. If you process batches (queue jobs, re-indexing), serializing the loaded dictionary once to storage/framework/cache and rehydrating it beats re-parsing the text file per worker.
Extend the dictionary, don't post-process. Brand names, SKUs, and internal product codes fragment badly out of the box. Append domain terms to the shipped dictionary (or via the library's add-word hook if the version exposes one) rather than gluing pieces back together with regex afterwards.
Stopwords and punctuation are your job. Segment output routinely includes punctuation and high-frequency noise (的, 了, 和). Filter with a config/stopwords.php array after segmentation; keep the list in your app, not in vendor/.
Search integration. Segment the query, then either build LIKE '%term%' clauses per term, or (better) push the segmented terms into Meilisearch/TNTSearch/Elasticsearch with a proper CJK analyzer. Segmentation output is also a cheap source of tags, related-article links, and sitemap keyword pages.
Bulk pipelines. For indexing jobs, segment in chunks and unset() the instance between chunks — long-lived processes accumulate result state across SetSource calls in some builds.
mb_convert_encoding($s, 'UTF-8', 'auto') at the boundary.getcwd() or a hardcoded relative path. Under queue workers, Octane, or a differently-rooted CLI this fails or loads nothing. Verify the loaded entry count after LoadDict() and patch the path to use __DIR__ if needed.memory_get_usage() before deploying to a small php-fpm pool, and never load it in a request that doesn't segment anything.GetFinallyResult() may return accumulated results across calls in some versions. Re-instantiate for safety, or explicitly clear result state, and write a test that segments two different strings in sequence.GetFinallyKeywords() is a heuristic, not TF-IDF. Its ordering is roughly frequency-and-length based. Don't use it to rank search results or compute relatedness scores — segment first, then score with your own weights.composer.json, and consider vendoring the single class into app/Support/ behind your own interface so you can swap to fukuball/jieba-php, the scws extension, or an Elasticsearch IK analyzer without touching call sites.GetFinallyResult() joins it, and diff against a known-good segmentation of the same sentence. If every character comes back as its own token, the dictionary didn't load — not an algorithm problem.PhpAnalysis to override matching or the keyword weighting is the only real hook; upstream a fork if you need behavior changes, and add tests the package doesn't ship.How can I help you explore Laravel packages today?