Current section
Files
Jump to
Current section
Files
lib/jieba.ex
defmodule Jieba.JiebaError do
defexception [:message]
end
defmodule Jieba.Keyword do
@moduledoc """
Corresponds to a Keyword in the jieba-rs API.
`keyword` is the keyword produced by the jieba-rs TextRank extractor.
`weight` is the score assigned by the TextRank extractor for the keyword.
"""
defstruct keyword: "",
weight: 0.0
end
defmodule Jieba.Tag do
@moduledoc """
Corresponds to a Tag in the jieba-rs API.
`word` is the string for the token
`tag` is a string with the tag for the token. These are often parts of speech tags.
"""
defstruct word: "",
tag: ""
end
defmodule Jieba.Token do
@moduledoc """
Corresponds to a Token in the jieba-rs API.
`word` is the string for the token
`start` and `end` are the locations in the input sentence of `word`.
"""
defstruct word: "",
start: 0,
end: 0
end
defmodule Jieba do
@moduledoc """
Wrapper for the [jieba-rs](https://github.com/messense/jieba-rs) project,
a Rust implementation of the Python [Jieba](https://github.com/fxsjy/jieba)
Chinese Word Segmentation library.
This module directly mostly projects the Rust API into Elixir with an
object-oriented imperative API with very thin syntactic sugar. Where possible,
it attempts to preserve functional behavior but this is not possible always,
especially with the `load_dict/2` interface.
"""
mix_config = Mix.Project.config()
version = mix_config[:version]
github_url = mix_config[:package][:links]["GitHub"]
use RustlerPrecompiled,
otp_app: :jieba,
crate: :rustler_jieba,
base_url: "#{github_url}/releases/download/v#{version}",
targets:
Enum.uniq(["aarch64-unknown-linux-musl" | RustlerPrecompiled.Config.default_targets()]),
# Jieba-rs is horridly slow in debug. Don't use it.
mode: :release,
version: version,
force_build: System.get_env("JIEBA_FORCE_RUSTLER_BUILD") in ["1", "true"]
@enforce_keys [:use_default, :dict_paths, :native]
defstruct [:use_default, :dict_paths, :native]
@type t :: %__MODULE__{
use_default: boolean(),
dict_paths: list(String.t()),
native: reference()
}
@doc """
Creates an initializes new Jieba instance.
This is a convenience wrapper that avoids the need to touch the imperative load_dict/2
API allowing for a more functional calling style.
Returns {:ok, jieba} or {:error, reason}
## Examples
iex> {:ok, jieba} = Jieba.new(use_default: false)
iex> Jieba.cut(jieba, "呢個係乜嘢呀")
["呢", "個", "係", "乜", "嘢", "呀"]
iex> {:ok, jieba} = Jieba.new()
iex> Jieba.cut(jieba, "呢個係乜嘢呀")
["呢", "個", "係", "乜嘢", "呀"]
iex> Jieba.cut(jieba, "李小福是创新办任也是云计算方面的家")
["李小福", "是", "创新", "办任", "也", "是", "云", "计算",
"方面", "的", "家"]
iex> {:ok, jieba} = Jieba.new(dict_paths: ["test/example_userdict.txt"])
iex> Jieba.cut(jieba, "李小福是创新办任也是云计算方面的家")
["李小福", "是", "创新办", "任", "也", "是", "云", "计算",
"方面", "的", "家"]
iex> {:error, :enoent} = Jieba.new(dict_paths: ["NotAFile.nope.nope"])
"""
def new(options \\ [{:dict_paths, []}, {:use_default, true}]) do
use_default = options[:use_default] != false
Enum.reduce(
options[:dict_paths] || [],
{:ok, if(use_default, do: native_new(), else: native_empty())},
fn path, result ->
case result do
{:ok, jieba} -> load_dict(jieba, path)
_ -> result
end
end
)
end
@doc """
Creates an initializes new Jieba instance using new/2.
Returns Jieba instance.
Raises Jieba.JiebaError on error.
## Examples
iex> jieba = Jieba.new!(use_default: false)
iex> Jieba.cut(jieba, "呢個係乜嘢呀")
["呢", "個", "係", "乜", "嘢", "呀"]
"""
def new!(options \\ [{:dict_paths, []}, {:use_default, true}]) do
case new(options) do
{:ok, jieba} -> jieba
{:error, reason} -> raise Jieba.JiebaError, message: to_string(reason)
end
end
# Since the new/3 convenience wrapper plus default options captures all
# the functionality this is private. However this is still needed to
# implement new/3.
defp native_new(), do: :erlang.nif_error(:nif_not_loaded)
# Creates an initializes new Jieba instance with an empty dictionary.
#
# Returns Jieba instance.
defp native_empty(), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Makes another Jieba with the same dictionary state.
Returns Jieba instance.
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.clone(jieba)
"""
def clone(_jieba), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Merges the entries in `_dict_path` to the current native Jieba instance
This is an imperative function and will modify the underlying state of the
Jieba instance. The function will return a new Jieba that has the
`Jieba.dict_paths` field updated. However, since both the original
input object and the returned object will share a native jieba instance,
they will both be altered even if the `dict_paths` field of the original
object is left untouched. The original object should be discarded after
use.
If you wish to keep the original Jieba instance, use `clone/1` to make
a copy that can be preserved.
Returns `(:ok, rust_jieba)`
## Examples
# Show that the original jieba instance and the returned ones are entangled even
# if the dict_paths are different.
iex> jieba = Jieba.new!()
iex> {:ok, new_jieba} = Jieba.load_dict(jieba, "test/example_userdict.txt")
iex> jieba.dict_paths
[]
iex> new_jieba.dict_paths
["test/example_userdict.txt"]
iex> new_jieba.native == jieba.native
true
# Show the effect of the entanglement on the cut/3 function.
iex> jieba = Jieba.new!()
iex> old_cut = Jieba.cut(jieba, "李小福是创新办任也是云计算方面的家", true)
["李小福", "是", "创新", "办任", "也", "是", "云", "计算",
"方面", "的", "家"]
iex> {:ok, new_jieba} = Jieba.load_dict(jieba, "test/example_userdict.txt")
iex> new_cut = Jieba.cut(new_jieba, "李小福是创新办任也是云计算方面的家", true)
["李小福", "是", "创新办", "任", "也", "是", "云", "计算",
"方面", "的", "家"]
iex> new_cut == old_cut
false
iex> old_cut == Jieba.cut(jieba, "李小福是创新办任也是云计算方面的家", true)
false
# Show error handling
iex> jieba = Jieba.new!()
iex> {:error, :enoent} = Jieba.load_dict(jieba, "NotAFile.nope.nope")
iex> {:error, invalid_entry} = Jieba.load_dict(jieba, "test/malformed_userdict.txt")
iex> invalid_entry
"line 5 `This is an invalid entry.\\n` frequency is is not a valid integer: invalid digit found in string"
"""
def load_dict(_jieba, _dict_path), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Merges the entries in `dict_path` to the current native Jieba instance using load_dict/2.
Please see load_dict/2 for caveats about imperative behavior.
Returns Jieba instance with merged dictionary.
Raises Jieba.JiebaError on error.
## Examples
iex> jieba = Jieba.new!()
iex> new_jieba = Jieba.load_dict!(jieba, "test/example_userdict.txt")
iex> Jieba.cut(new_jieba, "李小福是创新办任也是云计算方面的家", true)
["李小福", "是", "创新办", "任", "也", "是", "云", "计算",
"方面", "的", "家"]
"""
def load_dict!(rust_jieba, dict_path) do
case load_dict(rust_jieba, dict_path) do
{:ok, jieba} -> jieba
{:error, reason} -> raise Jieba.JiebaError, message: to_string(reason)
end
end
@doc """
Given a new segment, this attempts to guess the frequency of the segment.
It is used by `add_word()` if no `freq` is given.
This can be used to examine the frequencies of the existing table which
can be helpful for tuning or even scaling datasets for dialects of Chinese
without as much corpus data. For example, if you had a small 粵語 dataset,
you could look up common characters like 雨,車,窗 that are not likely to
have a huge frequency divergence (as opposed to things like 他 or 地 which
while frequent are far more used in some dialects), find the average delta
and then scale up the frequencies to match the dictionary you are merging
in to.
Returns 483 (frequency of the word)
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.suggest_freq(jieba, "也")
307852
iex> Jieba.suggest_freq(jieba, "是")
796991
iex> Jieba.suggest_freq(jieba, "也是")
4083
iex> Jieba.suggest_freq(jieba, "佢哋")
1
"""
def suggest_freq(_jieba, _segment), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Adds a segment to the dictionary with an optional frequency or tag.
If no frequency is given, `suggest_freq()` is used to guess the frequency.
This can be used to prevent over-segmentation.
Returns 2434 (frequency of the added segment)
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.cut(jieba, "「台中」正确应该不会被切开", true)
["「", "台", "中", "」", "正确", "应该", "不会", "被", "切开"]
iex> Jieba.add_word(jieba, "台中", nil, nil)
69
iex> Jieba.cut(jieba, "「台中」正确应该不会被切开", true)
["「", "台中", "」", "正确", "应该", "不会", "被", "切开"]
"""
def add_word(_jieba, _word, _freq, _tag), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence and breaks it into a vector of segments.
Returns `["李小福", "是"]`
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.cut(jieba, "李小福是创新办任也是云计算方面的家", true)
["李小福", "是", "创新", "办任", "也", "是", "云", "计算",
"方面", "的", "家"]
"""
def cut(jieba, sentence, hmm \\ true) do
native_cut(jieba, sentence, hmm)
end
defp native_cut(_jieba, _sentence, _hmm), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Uses Rust-side, process-global, Jieba instance to break a sentence into a
vector of segments without hmm disabled.
This method is not guaranteed to be thread-safe on the rust-side since the
Jieba_rs API does specify thread-safety guarantees on multiple calls to cut.
Also, this API does not allow for loading custom dictionaries and the
disabling of hmm often yields over-segmentation.
Prefer cut/3 instead.
Returns `["李小福", "是"]`
## Examples
iex> Jieba.cut("李小福是创新办任也是云计算方面的家")
["李", "小", "福", "是", "创新", "办", "任", "也", "是", "云", "计算", "方面", "的", "家"]
"""
@deprecated "Use cut/3 instead"
def cut(sentence) do
native_static_cut(sentence)
end
defp native_static_cut(_sentence), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence and breaks it into a vector containing segments using
the most aggressive segmentation possible given the input dictionary.
It is likely that it will produce an over-segmented results. There may
also be multiple tokens returned any sequence of characters. For example,
`"创新"` will return `["创", "创新", "新"]`.
This means that joining all elements of the result vector will not
necessarily result in a string with the same meaning as the input.
Returns `["李", "小", "福", "是", "创", "创新", ...]`
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.cut_all(jieba, "李小福是创新办任也是云计算方面的家")
["李", "小", "福", "是", "创", "创新", "新", "办", "任", "也", "是", "云", "计", "计算", "算", "方", "方面", "面", "的", "家"]
"""
def cut_all(_jieba, _sentence), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence and breaks it into a vector containing segments tuned for
search engine keyword matching. This tends to produce shorted segments
that are more likely to produce a keyword match. For example,
`"中国科学院"` will produce `["中国", "科学", "学院", "科学院", "中国科学院"]`
whereas with `cut()`, it will just produce `["中国科学院"]`
It is possible (and likely) that phrases will be repeated.
Returns `["中国", "科学", "学院", "科学院", "中国科学院"]`
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.cut(jieba, "小明硕士毕业于中国科学院计算所,后在日本京都大学深造", true)
["小明", "硕士", "毕业", "于", "中国科学院", "计算所", ",", "后", "在", "日本京都大学", "深造"]
iex> Jieba.cut_for_search(jieba, "小明硕士毕业于中国科学院计算所,后在日本京都大学深造", true)
["小明", "硕士", "毕业", "于", "中国", "科学", "学院", "科学院", "中国科学院", "计算",
"计算所", ",", "后", "在", "日本", "京都", "大学", "日本京都大学", "深造"]
"""
def cut_for_search(_jieba, _sentence, _hmm), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence, performs `cut()`, and then produces a vector of `Token` structs that
indicate where in the sentence the segment comes from.
The tokenization mode can be one of :default or :search.
Returns `[
%{start: 0, __struct__: Jieba.Token, word: "李小福"},
%{start: 3, __struct__: Jieba.Token, word: "是"},["是"]`
]`
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.tokenize(jieba, "李小福是创新办任也是云计算方面的家", :default, true)
[
%{start: 0, __struct__: Jieba.Token, word: "李小福"},
%{start: 3, __struct__: Jieba.Token, word: "是"},
%{start: 4, __struct__: Jieba.Token, word: "创新"},
%{start: 6, __struct__: Jieba.Token, word: "办任"},
%{start: 8, __struct__: Jieba.Token, word: "也"},
%{start: 9, __struct__: Jieba.Token, word: "是"},
%{start: 10, __struct__: Jieba.Token, word: "云"},
%{start: 11, __struct__: Jieba.Token, word: "计算"},
%{start: 13, __struct__: Jieba.Token, word: "方面"},
%{start: 15, __struct__: Jieba.Token, word: "的"},
%{start: 16, __struct__: Jieba.Token, word: "家"}
]
"""
def tokenize(_jieba, _sentence, _mode, _hmm), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence, performs `cut()`, and then produces a vector of `Tag` structs that
label each segment with a tag -- usually the part of speech.
Returns `[
%Jieba.Tag{word: "李小福", tag: "x"},
%Jieba.Tag{word: "是", tag: "v"},
...
]`
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.tag(jieba, "李小福是创新办任也是云计算方面的家", true)
[
%Jieba.Tag{word: "李小福", tag: "x"},
%Jieba.Tag{word: "是", tag: "v"},
%Jieba.Tag{word: "创新", tag: "v"},
%Jieba.Tag{word: "办任", tag: "x"},
%Jieba.Tag{word: "也", tag: "d"},
%Jieba.Tag{word: "是", tag: "v"},
%Jieba.Tag{word: "云", tag: "ns"},
%Jieba.Tag{word: "计算", tag: "v"},
%Jieba.Tag{word: "方面", tag: "n"},
%Jieba.Tag{word: "的", tag: "uj"},
%Jieba.Tag{word: "家", tag: "q"}
]
"""
def tag(_jieba, _sentence, _hmm), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Takes a sentence, and attempts to extract the top_k keywords based on TFIDF
ranking.
This constructs a new TFIDF struct each call. If there is no `tfidf_dict_path`
or `_stop_words`, this is lightweight. However, if there is, it will
reconstruct the internal data structure per call.
We cannot do better yet because the Jieba-RS interface confusingly requires
that the TFIDF struct must be bound to a stack-scoped Jieba object. With
the Elixir bridge. the Jieba object is actually dynamically allocated and
shared with a ResourceARC and a Mutex. To do this more properly would
require the Jieba-RS project redesign the TFIDF interface so it takes
`jieba` on the `extract_keyword()` method and not as in the `new()` method.
See https://github.com/messense/jieba-rs/issues/99 for details.
Returns { :ok,
[ %Jieba.Keyword{keyword: "北京烤鸭", weight: 1.3904870323222223},
%Jieba.Keyword{keyword: "纽约", weight: 1.121759684755},
%Jieba.Keyword{keyword: "天气", weight: 1.0766573240983333}
]
}
## Examples
iex> jieba = Jieba.new!()
iex> {:ok, top_k_tags} = Jieba.tfidf_extract_tags(jieba, "今天纽约的天气真好啊,京华大酒店的张尧经理吃了一只北京烤鸭。后天纽约的天气不好,昨天纽约的天气也不好,北京烤鸭真好吃", 3)
iex> top_k_tags
[%Jieba.Keyword{keyword: "北京烤鸭", weight: 1.3904870323222223}, %Jieba.Keyword{keyword: "纽约", weight: 1.121759684755}, %Jieba.Keyword{keyword: "天气", weight: 1.0766573240983333}]
iex> jieba = Jieba.new!()
iex> {:error, :enoent} = Jieba.tfidf_extract_tags(jieba, "", 3, [], "NotAFile.nope.nope")
"""
def tfidf_extract_tags(
_jieba,
_sentence,
_top_k,
_allowed_pos \\ [],
_tfidf_dict_path \\ "",
_stop_words \\ []
),
do: :erlang.nif_error(:nif_not_loaded)
@doc """
Uses TFIDF algorithm to extract tags.
Returns list of Jieba.Keyword
Raises Jieba.JiebaError on error.
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.tfidf_extract_tags!(jieba, "今天纽约的天气真好啊,京华大酒店的张尧经理吃了一只北京烤鸭。后天纽约的天气不好,昨天纽约的天气也不好,北京烤鸭真好吃", 3)
[%Jieba.Keyword{keyword: "北京烤鸭", weight: 1.3904870323222223}, %Jieba.Keyword{keyword: "纽约", weight: 1.121759684755}, %Jieba.Keyword{keyword: "天气", weight: 1.0766573240983333}]
"""
def tfidf_extract_tags!(
jieba,
sentence,
top_k,
allowed_pos \\ [],
tfidf_dict_path \\ "",
stop_words \\ []
) do
case tfidf_extract_tags(jieba, sentence, top_k, allowed_pos, tfidf_dict_path, stop_words) do
{:ok, tags} -> tags
{:error, reason} -> raise Jieba.JiebaError, message: to_string(reason)
end
end
@doc """
Takes a sentence, and attempts to extract the top_k keywords based on TextRank
ranking.
This constructs a new TextRank struct each call. If there is no `_stop_words`,
this is lightweight. However, if there is, it will reconstruct the internal
data structure per call.
We cannot do better yet because the Jieba-RS interface confusingly requires
that the TextRank struct must be bound to a stack-scoped Jieba object. With
the Elixir bridge. the Jieba object is actually dynamically allocated and
shared with a ResourceARC and a Mutex. To do this more properly would
require the Jieba-RS project redesign the TextRank interface so it takes
`jieba` on the `extract_keyword()` method and not as in the `new()` method.
See https://github.com/messense/jieba-rs/issues/99 for details.
Returns { :ok,
[
%Jieba.Keyword{keyword: "天气", weight: 19307118367.17687},
%Jieba.Keyword{keyword: "纽约", weight: 19179632457.07701},
%Jieba.Keyword{keyword: "不好", weight: 13769629783.10484}
]
}
## Examples
iex> jieba = Jieba.new!()
iex> {:ok, top_k_tags } = Jieba.textrank_extract_tags(jieba, "今天纽约的天气真好啊,京华大酒店的张尧经理吃了一只北京烤鸭。后天纽约的天气不好,昨天纽约的天气也不好,北京烤鸭真好吃", 3)
iex> top_k_tags
[ %Jieba.Keyword{keyword: "天气", weight: 19307118367.17687}, %Jieba.Keyword{keyword: "纽约", weight: 19179632457.07701}, %Jieba.Keyword{keyword: "不好", weight: 13769629783.10484} ]
"""
def textrank_extract_tags(_jieba, _sentence, _top_k, _allowed_pos \\ [], _stop_words \\ []),
do: :erlang.nif_error(:nif_not_loaded)
@doc """
Uses TextRank algorithm to extract tags.
Returns list of Jieba.Keyword
Raises Jieba.JiebaError on error.
## Examples
iex> jieba = Jieba.new!()
iex> Jieba.textrank_extract_tags!(jieba, "今天纽约的天气真好啊,京华大酒店的张尧经理吃了一只北京烤鸭。后天纽约的天气不好,昨天纽约的天气也不好,北京烤鸭真好吃", 3)
[ %Jieba.Keyword{keyword: "天气", weight: 19307118367.17687}, %Jieba.Keyword{keyword: "纽约", weight: 19179632457.07701}, %Jieba.Keyword{keyword: "不好", weight: 13769629783.10484} ]
"""
def textrank_extract_tags!(jieba, sentence, top_k, allowed_pos \\ [], stop_words \\ []) do
case textrank_extract_tags(jieba, sentence, top_k, allowed_pos, stop_words) do
{:ok, tags} -> tags
{:error, reason} -> raise Jieba.JiebaError, message: to_string(reason)
end
end
end