cad.net 倒序索引
概念性文章,不做任何运行保证,只做原理设计
查找字符串
构造倒序索引解决查找替换字符串速度慢问题,
它是单线程方案,只是查询不同doc的key可以并行多线程.
例如如何从十万个dwg里面找到"2004年建筑规范",
需要构造map来储存文件路径,文字所属的句柄,
同时最好了解一些Everything原理来避免遍历磁盘文件.
1,打开图纸后遍历全部文本,
通过分词器分词,写入字典,
key是词,value是文字的id集合(一词多行).
2,通过不同的事件进行维护索引,
事件有两种,一种是图元事件,一种是数据库事件,
用后者比较简单,不需要更改每个图元.
数据库加入事件/数据库移除事件/数据库更改事件.
撤回和重做本质只是删除和更改!!也会触发对应的事件的!!
3,高频替换时候,就可以通过这个字典进行了.
通过id找词叫正序索引,而反之就是倒序索引.
把找到的多个ids合并,就是命中的语句们了.
这就是人们常说的用空间换时间的方式.也是搜索引擎的原理.
基本上全部CAD二次索引都是这样做.
你必须要用单线程方案,
否则并行遍历块表无法重置迭代器(除非找到更改指针方案).
你会发现桌子就是少做很多索引组织表
,
然后导致你需要各种遍历.
原理
原理的视频:
ES数据库: https://b23.tv/2Jwi1gG
.NET分词器很多的,只需要选择其中一款就好了,并且需要支持中文.
https://www.cnblogs.com/linezero/p/jiebanetcore.html
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var segmenter = new JiebaSegmenter();
var segments = segmenter.Cut("我来到广州华南理工大学", cutAll: true);
Console.WriteLine("【全模式】:{0}", string.Join("/ ", segments));
CAD事件,这个网站好看点:
http://mac.bb-mac.com/help/books/.net/AutoCAD.net/files/WS1a9193826455f5ff-e569a0121d1945c08-2024.htm
代码
此代码没有经过本人测试,不知道事件顺序,
所以可能根本跑不起来,只做原理说明而已
public class SddSetCommands {
HashSet<SingleDatabaseDictionary> SddSet = new();
[IFoxInitialize]
public void Initialize() {
// 启动的两种模式都要做:
// 1,通过注册表启动,必须用文档管理器加载文档事件,
// 等待界面完成,直到文档出现后触发事件,然后扫掠全部文档.
// 2,通过netload加载启动,需要尝试直接加入.
var dm = Acap.DocumentManager;
if (dm is null) return;
if (dm.Count != 0) {
foreach (Document doc in dm) {
HashAdd(doc);
}
}
// dm的是全局事件可以不卸载,doc和db事件则需要注意卸载
dm.DocumentCreated += DmCreated;
dm.DocumentToBeDestroyed -= DmDestroyed;
}
// 打开文档,如果数据库已经存盘就会加入,否则跳过
void DmCreated(object sender, DocumentCollectionEventArgs e) {
Env.Printl("DmCreated");
HashAdd((Document)sender);
}
// 文档关闭就释放对应的字典
void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
Env.Printl("DmDestroyed");
var doc = (Document)sender;
LoadCommandEnded(doc, false);
SddSet.Remove(new(doc.Database));
}
void HashAdd(Document doc) {
if (doc is null)
throw new ArgumentNullException("doc is null");
if (doc.IsReadOnly) return;
// 未保存不加入
var file = Path.Combine(Path.GetDirectoryName(doc.Database.OriginalFileName), doc.Name);
var originEx = Path.GetExtension(file).ToLower();
if (originEx != ".dwg" || originEx != ".dxf") return;
var dict = new SingleDatabaseDictionary(doc.Database);
if (!SddSet.Contains(dict)) {
SddSet.Add(dict);
dict.Builder();
}
LoadCommandEnded(doc);
}
// todo 命令事件改为保存事件
bool LoadCommandEnded(Document doc, bool isload = true) {
if(isload) doc.CommandEnded += DocCommandEnded;
else doc.CommandEnded -= DocCommandEnded;
}
/// <summary>
/// 命令完成后(内锁文档)
/// </summary>
void DocCommandEnded(object sender, CommandEventArgs e) {
// 过滤噪声
if (string.IsNullOrEmpty(e.GlobalCommandName)
|| e.GlobalCommandName == "#")
return;
Env.Printl("DocCommandEnded");
Env.Printl(e.GlobalCommandName.ToUpper());
var doc = (Document)sender;
switch (e.GlobalCommandName.ToUpper()) {
case "SAVEAS":
// var num = Acap.GetSystemVariable("DBMode");
// if (num == ?)
// 保存数据库,就加入分析.
// 如果是关闭时候保存,就不加入啊.(不知道如何实现)
// 反正重复加入不进去HashSet
HashAdd(doc);
break;
}
}
// 查询
public void Find(string txt, Action<Database, HashSet<ObjectId>> action) {
var segments = SingleDatabaseDictionary.Segmenter.Cut(txt, cutAll: true); // 分词
Env.Printl("查询器分词:" + string.Join(" ", segments));
// 遍历每个数据库的字典(可以改为并行)
foreach (var dict in SddSet) {
// 命中词汇的行(文本ObjectId)
HashSet<ObjectId> set = new();
// 遍历每个词,多个词可能在同一行,通过hashset过滤
// 搜索引擎还可以根据出现数量来作为关联度,以此置顶.
foreach (var se in segments) {
if (dict.Words.TryGetValue(se, out var ids))
set.UnionWith(ids);
}
// Env.Printl("数据库:{dict.DwgFile}");
// Env.Printl("id是:{string.Join(" ", set)}");
action.Invoke(dict.Database, set); // 数据库用于事务,set用来修改
}
}
}
public class SingleDatabaseDictionary {
// 公开字段用于序列化
public static JiebaSegmenter Segmenter = new(); // 分词器
public Database Database;
public string DwgFile => Database.Filename; // 不缓存,保存之后会变
public Dictionary<string, HashSet<ObjectId>> Words; // 倒序索引
public override int GetHashCode() {
return DwgFile.GetHashCode();
}
public override bool Equals(object obj) {
if (obj is SingleDatabaseDictionary other)
return DwgFile == other.DwgFile;
return false;
}
public SingleDatabaseDictionary(Database db) {
if (db is null)
throw new ArgumentNullException("db is null");
Database = db;
Words = new();
}
// 直接打开图纸后遍历构建倒序索引
public void Builder() {
// 通过数据库事件,就不用附加事件到图元了
Database.ObjectAppended += DbAppended; // todo没有注销
Database.ObjectErased += DbErased; // 撤回时候删除
Database.ObjectModified += DBModified; // 撤回时候更改
Database.ObjectUnappended += DbUnappended;
Database.ObjectReappended += DbReappended;
using DBTrans tr = new(Database);
foreach (var bid in tr.BlockTable) {
if (!bid.IsOk()) continue;
using var btr = tr.GetObject<BlockTableRecord>(bid, OpenMode.ForRead);
foreach (var eid in btr) {
if (!eid.IsOk()) continue;
using var ent = tr.GetObject<Entity>(eid, OpenMode.ForRead);
if (ent is DBText dbtext) {
// ent.Modified += EntModified; // todo没有注销
// 和下面冲突 ent.ModifyUndone += EntOpenedForModify;
// ent.OpenedForModify += EntOpenedForModify;
AddNewly(dbtext.ObjectId, dbtext.TextString);
}
else if (ent is MText mtext) {
// ent.Modified += EntModified;
// 和下面冲突 ent.ModifyUndone += EntOpenedForModify;
// ent.OpenedForModify += EntOpenedForModify;
AddNewly(mtext.ObjectId, mtext.Text);
}
}
}
}
// 数据库内容新增
private void DbAppended(object sender, ObjectEventArgs e) {
Env.Printl("DbAppended");
if (e.DBObject is DBText dbtext) {
AddNewly(dbtext.ObjectId, dbtext.TextString);
}
else if (e.DBObject is MText mtext) {
AddNewly(mtext.ObjectId, mtext.Text);
}
}
/// <summary>
/// 撤回事件(获取删除对象)
/// 它会获取有修改步骤的图元id
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DbErased(object sender, ObjectErasedEventArgs e) {
Env.Printl("DbErased");
// if (!State.IsRun) return;
if (e.Erased) return; // 跳过,否则无法读取图元信息
if (e.DBObject is DBText dbtext) {
RemoveOld(dbtext.ObjectId, dbtext.TextString);
}
else if (e.DBObject is MText mtext) {
RemoveOld(mtext.ObjectId, mtext.Text);
}
}
/// <summary>
/// 撤回事件(更改时触发)
/// 它会获取有修改步骤的图元id
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DBModified(object sender, ObjectEventArgs e) {
Env.Printl("DBModified");
// if (!State.IsRun) return;
if (!e.DBObject.IsUndoing || e.DBObject.IsErased) return;
DbAppended(sender, e);
}
// 撤回事件,拉伸填充没有此事件,可能不需要.
// 只需要上面两个
private void DbUnappended(object sender, ObjectEventArgs e) {
Env.Printl("DbUnappended");
// DbErased(sender, e);
}
// 撤回后重做,拉伸填充没有此事件,可能不需要.
// 重做不过也是删除和修改,可能不需要
private void DbReappended(object sender, ObjectEventArgs e) {
Env.Printl("DbReappended");
// DbAppended(sender, e);
}
// 图元修改前,用数据库事件代替
//private void EntOpenedForModify(object sender, EventArgs e) {
// DbErased(sender, e);
//}
//// 图元修改后,用数据库事件代替
//private void EntModified(object sender, EventArgs e) {
// DbAppended(sender, e);
//}
private void RemoveOld(ObjectId id, string txt) {
var segments = Segmenter.Cut(txt, cutAll: true);
foreach (var item in segments) {
if (Words.ContainsKey(item)) {
Words[item].Remove(id);
if (Words[item].Count == 0)
Words.Remove(item);
}
}
}
private void AddNewly(ObjectId id, string txt) {
var segments = Segmenter.Cut(txt, cutAll: true);
foreach (var item in segments) {
if (!Words.ContainsKey(item))
Words.Add(item, new HashSet<ObjectId> { id });
else
Words[item].Add(id);
}
}
}
嵌套组处理
因为深度克隆带组的图元,手选克隆后其中一个新图元,
会和旧组一起选择,但是新图元并没有加入组,
怀疑是图元上面记录了组,而不是组记录了对象.
解决方案一:移除图元内记录.
解决方案二:克隆前记录原本的组的成员,解组,再克隆,最后创建组还原.
解决方案三:浅克隆不会有这些问题.
浅克隆
using DBTrans tr = new();
var getRes = Env.Editor.GetSelection();
if (getRes.Status != PromptStatus.OK) return;
// [旧组名,旧组内图元]
var gs = new Dictionary<string, ObjectId[]>();
// [旧图元,新图元]
var idMap = new Dictionary<ObjectId, ObjectId>();
var getEnts = getRes.Value.GetEntities<Entity>();
foreach (var ent in getEnts) {
idMap.Add(ent.Id, tr.CurrentSpace.AddEntity(ent.CloneEx()));
// 遍历图元的组,构造映射关系
foreach (var group in ent.GetGroups()) {
if (gs.ContainsKey(group.Name)) continue;
gs.Add(group.Name, group.GetAllEntityIds());
}
}
// 每次迭代同一个组的,然后通过字典获取新的,即为新组图元.
foreach (var ids in gs.Values)
tr.GroupDict.AddGroup("*U", ids.Select(id => idMap[id]));
组数量排序
public class GroupInfo : IEquatable<GroupInfo> {
public int Count {get; private set;}
public ObjectId ObjectId {get;}
public string Name {get;}
public GroupInfo(string name, ObjectId id, int count) {
Count = count;
ObjectId = id;
Name = name;
}
public void SetCount(int count){
Count = count;
}
// 利用它排序
public override int GetHashCode() {
return Count;
}
// 为了方案一,所以只比较id
public bool Equals(GroupInfo? other) {
if (other is null) return false;
return this.ObjectId.Equals(other.ObjectId);
}
public bool EqualsAll(GroupInfo? other) {
if (other is null) return false;
return this.ObjectId.Equals(other.ObjectId)
&& this.Count == other.Count
&& this.Name == other.Name;
}
}
public class GroupDictionary {
// 因为组是可以嵌套的,
// 嵌套的方式就是少到多,因此排序就找到最下层的.
// 倒序索引
// [图元id,组ids]
public Dictionary<ObjectId, SortedSet<GroupInfo>> IdMap = new();
public GroupDictionary(Database? db = null) {
using DBTrans tr = new(db);
using var groups = (DBDictionary)tr.GetObject(
db.GroupDictionaryId,OpenMode.ForRead);
// 遍历所有的组
foreach (DBDictionaryEntry entry in groups) {
using var group = (Group)tr.GetObject(entry.Value, OpenMode.ForRead);
var eids = group.GetAllEntityIds();
// 遍历组下的图元
foreach (var eid in eids) {
//using var ent = (Entity)tr.GetObject(eid, OpenMode.ForRead);
var gi = new GroupInfo(entry.Key, group.ObjectId, eids.Length);
if(!IdMap.ContainsKey(eid)){
IdMap.Add(eid, new(){gi});
}
else {
IdMap[eid].Add(gi);
}
}
}
}
}
命令
[CommandMethod(nameof(DeepCloneGroupCmd))]
public void DeepCloneGroupCmd() {
using DBTrans tr = new();
var filter = new SelectionFilter(new TypedValue[0]);
var ss = Env.Editor.GetSelection(filter);
if (ss.Status != PromptStatus.OK)
return;
ObjectId[] ids = ss.Value.GetObjectIds();
for (int i = 0; i < ids.Length; i++) {
using var ent = tr.GetObject<Entity>(ids[i], OpenMode.ForRead);
DeepCloneGroup(ent);
}
}
深克隆排序方案一
public void DeepCloneGroup(Entity sent) {
var tr = DBTrans.Top;
var IdMapper = sent.DeepCloneEx();
var ids = IdMapper.Keys; //克隆后新id....还是Values?
// [旧组,要创建同组的图元们] 此数据结构会排序
SortedDictionary<GroupInfo, List<ObjectId>> smap = new();
// 获取图元加入了哪个旧组,并移除.
foreach (var id in ids) {
using var ent = tr.GetObject(id, OpenMode.ForRead);
// 一个图元多个组
var gids = ent.GetGroups();
foreach (var gid in gids) {
using var group = (Group)tr.GetObject(gid, OpenMode.ForRead);
var length = group.GetAllEntityIds().Length;
group.UpgradeOpen();
group.Remove(id);
group.DowngradeOpen();
ent.RemoveGroup(group.ObjectId);
// 记录旧组信息
var gi = new GroupInfo(group.Name, group.ObjectId, length-1);
List<ObjectId> idList = new();
// ContainsKey调用GroupInfo.Equals,
// 因此Equals改成只比较组id.
// 若不改Equals可以用方案二倒腾结构,不过速度慢了.
// 先移除再加入才能根据length作为hash重排序
if (smap.ContainsKey(gi)) {
idList = samp[gi];
smap.Remove(gi);
}
idList.Add(id);
smap.Add(gi, idList);
}
}
// 根据排序创建新组,升序,少在前多在后
// 筛选出同组的新ids,根据不同组内数量,循环构建.
foreach(var item in smap) {
tr.GroupDict.AddGroup("*U", item.Pair.Value);
}
}
深克隆排序方案二
GroupInfo类Equals是全部字段比较的,
不需要改为只比较组id,当然,只比较也是可以的.
public void DeepCloneGroup(Entity sent) {
var tr = DBTrans.Top;
// var gd = new GroupDictionary(tr.Database);
// 深度克隆后的图元会自动加入组中
var IdMapper = ent.DeepCloneEx();
ids = IdMapper.Keys; //克隆后新id....还是Values?
// [图元,所属旧组们]
Dictionary<ObjectId, HashSet<GroupInfo>> map = new();
// 获取图元加入了哪个旧组,并移除.
foreach (var id in ids) {
using var ent = tr.GetObject(id, OpenMode.ForRead);
// 一个图元多个组
var gids = ent.GetAllGroupObjectIds();
foreach (var gid in gids) {
using var group = (Group)tr.GetObject(gid, OpenMode.ForRead);
group.UpgradeOpen();
group.Remove(id);
group.DowngradeOpen();
ent.RemoveGroup(group.ObjectId);
// 记录旧组信息
// ent不会重复记录组,所以这里其实不需要hashset<gi>
var gi = new GroupInfo(group.Name, group.ObjectId, -1);
if (!map.ContainsKey(id))
map.Add(id, new(){ gi });
else
map[id].Add(gi);
}
}
// 遍历组,设定组信息图元数量用于排序
foreach(var item in map) {
foreach(var gi in item.Pair.Value) {
if (gi.Count != -1) continue; // 跳过已经赋值的
using var group = (Group)tr.GetObject(gi.ObjectId, OpenMode.ForRead);
gi.SetCount(group.GetAllEntityIds().Length);
}
}
// [旧组,要创建同组的图元们] 此数据结构会排序
SortedDictionary<GroupInfo, List<ObjectId>> smap = new();
foreach (var item in map) {
foreach(var gi in item.Pair.Value) {
if (!smap.ContainsKey(gi))
smap.Add(gi, new(){ item.Key });
else
smap[gi].Add(item.Key);
}
}
// 根据排序创建新组,升序,少在前多在后
// 筛选出同组的新ids,根据不同组内数量,循环构建.
foreach(var item in smap) {
tr.GroupDict.AddGroup("*U", item.Pair.Value);
}
}
(完)